diff --git a/.metadata b/.metadata index 3bfa89d..0691157 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "bd7a4a6b5576630823ca344e3e684c53aa1a0f46" + revision: "9f455d2486bcb28cad87b062475f42edc959f636" channel: "stable" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 - base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 - - platform: web - create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 - base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 + create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + - platform: android + create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 # User provided section diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index eb848a5..6da2591 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,8 @@ android:label="tracking_app" android:name="${applicationName}" android:icon="@mipmap/ic_launcher"> + Bool { + GMSServices.provideAPIKey("AIzaSyBRplvYc2qNr0KuGUndmcJQHiVdBLIO1IA") GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } diff --git a/lib/app/config/di/di.config.dart b/lib/app/config/di/di.config.dart index ae51571..4891c79 100644 --- a/lib/app/config/di/di.config.dart +++ b/lib/app/config/di/di.config.dart @@ -61,12 +61,20 @@ import '../../../features/driver_orders_details/data/repos/order_details_repo_im as _i55; import '../../../features/driver_orders_details/domain/repos/order_details_repo.dart' as _i313; +import '../../../features/driver_orders_details/domain/usecases/get_address_usecase.dart' + as _i453; +import '../../../features/driver_orders_details/domain/usecases/get_driver_data_usecase.dart' + as _i883; import '../../../features/driver_orders_details/domain/usecases/get_order_details_usecase.dart' as _i1045; +import '../../../features/driver_orders_details/domain/usecases/get_real_route_usecase.dart' + as _i707; import '../../../features/driver_orders_details/domain/usecases/push_notification_usecase.dart' as _i809; import '../../../features/driver_orders_details/domain/usecases/send_device_notification_usecase.dart' as _i44; +import '../../../features/driver_orders_details/domain/usecases/update_driver_location_usecase.dart' + as _i294; import '../../../features/driver_orders_details/domain/usecases/update_order_state_usecase.dart' as _i727; import '../../../features/driver_orders_details/presentation/manager/order_details_cubit.dart' @@ -165,7 +173,19 @@ extension GetItInjectableX on _i174.GetIt { () => _i583.MyOrdersRemoteDataSourceImp(gh<_i890.ApiClient>()), ); gh.factory<_i313.OrderDetailsRepo>( - () => _i55.OrderDetailsRepoImpl(gh<_i114.OrderDetailsRemoteDatasource>()), + () => _i55.OrderDetailsRepoImpl( + gh<_i114.OrderDetailsRemoteDatasource>(), + gh<_i603.AuthStorage>(), + ), + ); + gh.factory<_i453.GetAddressUsecase>( + () => _i453.GetAddressUsecase(gh<_i313.OrderDetailsRepo>()), + ); + gh.factory<_i707.GetRealRouteUsecase>( + () => _i707.GetRealRouteUsecase(gh<_i313.OrderDetailsRepo>()), + ); + gh.factory<_i294.UpdateDriverLocationUsecase>( + () => _i294.UpdateDriverLocationUsecase(gh<_i313.OrderDetailsRepo>()), ); gh.factory<_i919.MyOrdersRepo>( () => _i754.MyOrdersRepoImpl(gh<_i466.MyOrdersRemoteDataSource>()), @@ -173,6 +193,9 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i335.GetOrderUseCase>( () => _i335.GetOrderUseCase(gh<_i919.MyOrdersRepo>()), ); + gh.factory<_i883.GetDriverDataUsecase>( + () => _i883.GetDriverDataUsecase(repo: gh<_i313.OrderDetailsRepo>()), + ); gh.factory<_i1045.GetOrderDetailsUsecase>( () => _i1045.GetOrderDetailsUsecase(repo: gh<_i313.OrderDetailsRepo>()), ); @@ -199,6 +222,18 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i712.AuthRepo>( () => _i566.AuthRepoImpl(gh<_i708.AuthRemoteDataSource>()), ); + gh.factory<_i375.OrderDetailsCubit>( + () => _i375.OrderDetailsCubit( + gh<_i1045.GetOrderDetailsUsecase>(), + gh<_i883.GetDriverDataUsecase>(), + gh<_i453.GetAddressUsecase>(), + gh<_i707.GetRealRouteUsecase>(), + gh<_i294.UpdateDriverLocationUsecase>(), + gh<_i727.UpdateOrderStateUsecase>(), + gh<_i809.PushNotificationUsecase>(), + gh<_i44.SendDeviceNotificationUsecase>(), + ), + ); gh.factory<_i991.ChangePasswordUsecase>( () => _i991.ChangePasswordUsecase(gh<_i712.AuthRepo>()), ); @@ -211,14 +246,6 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i112.VerifyResetCodeUsecase>( () => _i112.VerifyResetCodeUsecase(gh<_i712.AuthRepo>()), ); - gh.factory<_i375.OrderDetailsCubit>( - () => _i375.OrderDetailsCubit( - gh<_i1045.GetOrderDetailsUsecase>(), - gh<_i727.UpdateOrderStateUsecase>(), - gh<_i809.PushNotificationUsecase>(), - gh<_i44.SendDeviceNotificationUsecase>(), - ), - ); gh.factoryParam<_i466.VerifyResetCodeCubit, String, dynamic>( (email, _) => _i466.VerifyResetCodeCubit( gh<_i112.VerifyResetCodeUsecase>(), diff --git a/lib/app/core/router/app_router.dart b/lib/app/core/router/app_router.dart index 4f7f329..ff9bb45 100644 --- a/lib/app/core/router/app_router.dart +++ b/lib/app/core/router/app_router.dart @@ -4,7 +4,10 @@ import 'package:tracking_app/app/config/di/di.dart'; import 'package:tracking_app/app/core/router/route_names.dart'; import 'package:tracking_app/features/Onboarding/presentation/pages/onboardingScreen.dart'; import 'package:tracking_app/features/app_sections/presentation/pages/app_sections.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/location_type.dart'; import 'package:tracking_app/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/pages/location_page.dart'; +import 'package:tracking_app/features/home/presentation/pages/driverOrderScreen.dart'; import 'package:tracking_app/features/profile/data/models/driver_model.dart'; import 'package:tracking_app/features/profile/presentation/pages/edit_driver_profile_page.dart'; import 'package:tracking_app/features/profile/presentation/pages/edit_vehicle_page.dart'; @@ -14,7 +17,6 @@ import 'package:tracking_app/features/my_orders/presentation/pages/order_details import 'package:tracking_app/features/auth/presentation/apply/view/apply_view.dart'; import 'package:tracking_app/features/auth/presentation/forget_pass/manager/cubit/forget_pass_cubit.dart'; import 'package:tracking_app/features/auth/presentation/forget_pass/pages/forget_pass_page.dart'; -import 'package:tracking_app/features/auth/presentation/login/pages/loginScreen.dart'; import 'package:tracking_app/features/auth/presentation/reset_password/manager/reset_password_cubit.dart'; import 'package:tracking_app/features/auth/presentation/reset_password/pages/change_password_page.dart'; import 'package:tracking_app/features/auth/presentation/reset_password/pages/reset_password.dart'; @@ -22,7 +24,7 @@ import 'package:tracking_app/features/auth/presentation/verify_reset/manger/cubi import 'package:tracking_app/features/auth/presentation/verify_reset/pages/verify_reset_page.dart'; final GoRouter appRouter = GoRouter( - initialLocation: RouteNames.onboarding, + initialLocation: RouteNames.login, routes: [ GoRoute( path: RouteNames.changePassword, @@ -36,7 +38,7 @@ final GoRouter appRouter = GoRouter( GoRoute( path: RouteNames.login, - builder: (context, state) => const LoginScreen(), + builder: (context, state) => const DriverOrderScreen(), ), GoRoute( @@ -110,5 +112,13 @@ final GoRouter appRouter = GoRouter( return OrderDetailsPage(order: order); }, ), + + GoRoute( + path: RouteNames.locationPage, + builder: (context, state) { + final locationType = state.extra as LocationType; + return LocationPage(locationType: locationType); + }, + ), ], ); diff --git a/lib/app/core/router/route_names.dart b/lib/app/core/router/route_names.dart index c435505..36005a4 100644 --- a/lib/app/core/router/route_names.dart +++ b/lib/app/core/router/route_names.dart @@ -16,4 +16,5 @@ abstract class RouteNames { static const ordersDetailsPage = "/ordersDetails"; static const myOrders = "/myOrders"; static const orderDetails = "/orderDetails"; + static const locationPage = "/locationPage"; } diff --git a/lib/app/core/ui_helper/assets/images.dart b/lib/app/core/ui_helper/assets/images.dart index 8b1029b..2bb488a 100644 --- a/lib/app/core/ui_helper/assets/images.dart +++ b/lib/app/core/ui_helper/assets/images.dart @@ -13,4 +13,7 @@ class Assets { static const String imagesFilter = "assets/images/filter.png"; static const String imagesFlower = "assets/images/Flower.svg"; static const String delete = "assets/images/delete.png"; + static const String driverLocation = "assets/images/driver_location.png"; + static const String userLocation = "assets/images/user_location.png"; + static const String floweryLocation = "assets/images/flowery_location.png"; } diff --git a/lib/app/core/utils/app_launcher.dart b/lib/app/core/utils/app_launcher.dart new file mode 100644 index 0000000..a096dff --- /dev/null +++ b/lib/app/core/utils/app_launcher.dart @@ -0,0 +1,20 @@ +import 'package:url_launcher/url_launcher.dart'; + +abstract class AppLauncher { + static void launchPhone(String phoneNumber) async { + final Uri url = Uri(scheme: 'tel', path: phoneNumber); + if (await canLaunchUrl(url)) { + await launchUrl(url); + } + } + + static void launchWhatsApp(String phoneNumber) async { + String formattedPhone = phoneNumber.replaceAll(RegExp(r'[^0-9]'), ''); + + if (formattedPhone.startsWith('0')) { + formattedPhone = '20${formattedPhone.substring(1)}'; + } + final Uri url = Uri.parse("whatsapp://send?phone=$formattedPhone"); + await launchUrl(url, mode: LaunchMode.externalApplication); + } +} diff --git a/lib/features/auth/data/mappers/change_password_dto_mapper.dart b/lib/features/auth/data/mapper/change_password_dto_mapper.dart similarity index 100% rename from lib/features/auth/data/mappers/change_password_dto_mapper.dart rename to lib/features/auth/data/mapper/change_password_dto_mapper.dart diff --git a/lib/features/auth/data/repos/auth_repo_impl.dart b/lib/features/auth/data/repos/auth_repo_impl.dart index 071b909..9d48d33 100644 --- a/lib/features/auth/data/repos/auth_repo_impl.dart +++ b/lib/features/auth/data/repos/auth_repo_impl.dart @@ -2,7 +2,7 @@ import 'package:injectable/injectable.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/auth/data/datasource/auth_remote_datasource.dart'; import 'package:tracking_app/features/auth/data/mapper/vehicles_mapper.dart'; -import 'package:tracking_app/features/auth/data/mappers/change_password_dto_mapper.dart'; +import 'package:tracking_app/features/auth/data/mapper/change_password_dto_mapper.dart'; import 'package:tracking_app/features/auth/data/model/request/LoginRequest.dart'; import 'package:tracking_app/features/auth/data/model/response/LoginResponse.dart'; import 'package:tracking_app/features/auth/data/model/response/change_password_dto.dart'; diff --git a/lib/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart b/lib/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart index 2329d60..9e2dc51 100644 --- a/lib/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart +++ b/lib/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart @@ -1,9 +1,12 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:dio/dio.dart'; -import 'package:flutter/services.dart'; -import 'package:googleapis_auth/auth_io.dart'; +import 'package:flutter_polyline_points/flutter_polyline_points.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:injectable/injectable.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/drivers_dto.dart'; +import 'package:flutter/services.dart'; +import 'package:googleapis_auth/auth_io.dart'; import 'package:tracking_app/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart'; import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; @@ -14,8 +17,8 @@ class OrderDetailsRemoteDatasourceImpl implements OrderDetailsRemoteDatasource { OrderDetailsRemoteDatasourceImpl({ required FirebaseFirestore firestore, required Dio dio, - }) : _firestore = firestore, - _dio = dio; + }) : _dio = dio, + _firestore = firestore; @override ApiResult> getOrderStream(String orderId) { @@ -38,31 +41,100 @@ class OrderDetailsRemoteDatasourceImpl implements OrderDetailsRemoteDatasource { } @override - Future> updateOrderState({ - required String orderId, - required String state, - }) async { + ApiResult> getDriverData(String driverId) { try { - final querySnapshot = await _firestore - .collection('orders') - .where('orderId', isEqualTo: orderId) - .get(); - if (querySnapshot.docs.isNotEmpty) { - await querySnapshot.docs.first.reference.update({ - 'oder_dt.status': state, - }); - } else { - await _firestore.collection('orders').doc(orderId).update({ - 'oder_dt.status': state, - }); + final stream = _firestore + .collection('drivers') + .doc(driverId) + .snapshots() + .where((snapshot) => snapshot.exists && snapshot.data() != null) + .map((snapshot) { + return DriverDataDto.fromJson( + snapshot.data() as Map, + ); + }); + return SuccessApiResult>(data: stream); + } catch (e) { + return ErrorApiResult>(error: e.toString()); + } + } + + @override + Future> getLatLngFromAddress(String address) async { + try { + final response = await _dio.get( + "https://nominatim.openstreetmap.org/search", + queryParameters: { + "q": "$address, Egypt", + "format": "json", + "limit": 1, + "addressdetails": 1, + }, + options: Options(headers: {"User-Agent": "tracking_app"}), + ); + + final data = response.data; + + print("<<<<<<<< Geocode response: $data"); + + if (response.statusCode == 200 && data != null && data.isNotEmpty) { + double lat = double.parse(data[0]['lat']); + double lon = double.parse(data[0]['lon']); + + return SuccessApiResult(data: LatLng(lat, lon)); } - return SuccessApiResult(data: null); + return SuccessApiResult(data: null); } catch (e) { - return ErrorApiResult(error: e.toString()); + return ErrorApiResult(error: e.toString()); } } @override + Future>> getRealRoute( + LatLng myLocation, + LatLng destination, + ) async { + try { + final response = await _dio.get( + "https://router.project-osrm.org/route/v1/driving/" + "${myLocation.longitude},${myLocation.latitude};" + "${destination.longitude},${destination.latitude}", + queryParameters: {"overview": "full", "geometries": "polyline"}, + ); + + final data = response.data; + + if (response.statusCode == 200 && data['code'] == 'Ok') { + String encodedPolyline = data['routes'][0]['geometry']; + + List result = PolylinePoints.decodePolyline( + encodedPolyline, + ); + + List polylineCoordinates = result + .map((point) => LatLng(point.latitude, point.longitude)) + .toList(); + + return SuccessApiResult>(data: polylineCoordinates); + } + + return ErrorApiResult>(error: 'No route found'); + } catch (e) { + return ErrorApiResult>(error: e.toString()); + } + } + + @override + Future updateDriverLocation( + String driverId, + double lat, + double lng, + ) async { + await FirebaseFirestore.instance.collection('drivers').doc(driverId).update( + {"currentLocation.lat": lat, "currentLocation.lng": lng}, + ); + } + Future> pushNotification({ required String title, required String des, @@ -78,7 +150,6 @@ class OrderDetailsRemoteDatasourceImpl implements OrderDetailsRemoteDatasource { } } - @override Future> sendDeviceNotification({ required String userId, required String title, @@ -156,4 +227,28 @@ class OrderDetailsRemoteDatasourceImpl implements OrderDetailsRemoteDatasource { return ErrorApiResult(error: e.toString()); } } + + Future> updateOrderState({ + required String orderId, + required String state, + }) async { + try { + final querySnapshot = await _firestore + .collection('orders') + .where('orderId', isEqualTo: orderId) + .get(); + if (querySnapshot.docs.isNotEmpty) { + await querySnapshot.docs.first.reference.update({ + 'oder_dt.status': state, + }); + } else { + await _firestore.collection('orders').doc(orderId).update({ + 'oder_dt.status': state, + }); + } + return SuccessApiResult(data: null); + } catch (e) { + return ErrorApiResult(error: e.toString()); + } + } } diff --git a/lib/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart b/lib/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart index 6c2b9ca..0fe9f94 100644 --- a/lib/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart +++ b/lib/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart @@ -1,8 +1,17 @@ +import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/drivers_dto.dart'; import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; abstract class OrderDetailsRemoteDatasource { ApiResult> getOrderStream(String orderId); + ApiResult> getDriverData(String driverId); + Future> getLatLngFromAddress(String address); + Future updateDriverLocation(String driverId, double lat, double lng); + Future>> getRealRoute( + LatLng myLocation, + LatLng destination, + ); Future> updateOrderState({ required String orderId, required String state, diff --git a/lib/features/driver_orders_details/data/mapper/drivers_dto_mapper.dart b/lib/features/driver_orders_details/data/mapper/drivers_dto_mapper.dart new file mode 100644 index 0000000..5d63e20 --- /dev/null +++ b/lib/features/driver_orders_details/data/mapper/drivers_dto_mapper.dart @@ -0,0 +1,20 @@ +import 'package:tracking_app/features/driver_orders_details/data/models/drivers_dto.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; + +extension DriversDtoMapper on DriverDataDto { + DriverDataModel toDriversModel() { + return DriverDataModel( + name: name, + phone: phone, + id: id, + deviceToken: deviceToken, + currentLocation: currentLocation.toDriverLocationModel(), + ); + } +} + +extension DriverLocationDtoMapper on DriverLocationDto { + DriverLocationModel toDriverLocationModel() { + return DriverLocationModel(lat: lat, lng: lng); + } +} diff --git a/lib/features/driver_orders_details/data/models/drivers_dto.dart b/lib/features/driver_orders_details/data/models/drivers_dto.dart new file mode 100644 index 0000000..bdde436 --- /dev/null +++ b/lib/features/driver_orders_details/data/models/drivers_dto.dart @@ -0,0 +1,55 @@ +class DriverDataDto { + final String id; + final String name; + final String phone; + final String deviceToken; + final DriverLocationDto currentLocation; + + DriverDataDto({ + required this.id, + required this.name, + required this.phone, + required this.deviceToken, + required this.currentLocation, + }); + + factory DriverDataDto.fromJson(Map json) { + return DriverDataDto( + id: json['id'] ?? '', + name: json['name'] ?? '', + phone: json['phone'] ?? '', + deviceToken: json['deviceToken'] ?? '', + currentLocation: DriverLocationDto.fromJson( + json['currentLocation'] ?? {}, + ), + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'phone': phone, + 'deviceToken': deviceToken, + 'currentLocation': currentLocation.toJson(), + }; + } +} + +class DriverLocationDto { + final double lat; + final double lng; + + DriverLocationDto({required this.lat, required this.lng}); + + factory DriverLocationDto.fromJson(Map json) { + return DriverLocationDto( + lat: (json['lat'] ?? 0).toDouble(), + lng: (json['lng'] ?? 0).toDouble(), + ); + } + + Map toJson() { + return {'lat': lat, 'lng': lng}; + } +} diff --git a/lib/features/driver_orders_details/data/repos/order_details_repo_impl.dart b/lib/features/driver_orders_details/data/repos/order_details_repo_impl.dart index a11f4cf..437dfbb 100644 --- a/lib/features/driver_orders_details/data/repos/order_details_repo_impl.dart +++ b/lib/features/driver_orders_details/data/repos/order_details_repo_impl.dart @@ -1,24 +1,31 @@ +import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart'; +import 'package:tracking_app/features/driver_orders_details/data/mapper/drivers_dto_mapper.dart'; import 'package:tracking_app/features/driver_orders_details/data/mapper/order_dto_mapper.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/drivers_dto.dart'; import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/notcicationModel.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/notficationDevice.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/orderStates.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; import 'package:tracking_app/features/driver_orders_details/domain/repos/order_details_repo.dart'; -import 'package:tracking_app/features/driver_orders_details/domain/usecases/update_order_state_usecase.dart'; -import 'package:tracking_app/features/driver_orders_details/domain/usecases/push_notification_usecase.dart'; -import 'package:tracking_app/features/driver_orders_details/domain/usecases/send_device_notification_usecase.dart'; @Injectable(as: OrderDetailsRepo) class OrderDetailsRepoImpl implements OrderDetailsRepo { final OrderDetailsRemoteDatasource _remoteDataSource; - OrderDetailsRepoImpl(this._remoteDataSource); + final AuthStorage _authStorage; + OrderDetailsRepoImpl(this._remoteDataSource, this._authStorage); @override - ApiResult> getOrderDetails(String orderId) { + Future>> getOrderDetails() async { + final orderId = await _authStorage.getOrderId(); + if (orderId == null) { + return ErrorApiResult>(error: "No order ID found"); + } final result = _remoteDataSource.getOrderStream(orderId); switch (result) { @@ -32,6 +39,32 @@ class OrderDetailsRepoImpl implements OrderDetailsRepo { } @override + ApiResult> getDriverData(String driverId) { + final result = _remoteDataSource.getDriverData(driverId); + + switch (result) { + case SuccessApiResult>(): + return SuccessApiResult>( + data: result.data.map((dto) => dto.toDriversModel()), + ); + case ErrorApiResult>(): + return ErrorApiResult>(error: result.error); + } + } + + @override + Future> getLatLngFromAddress(String address) { + return _remoteDataSource.getLatLngFromAddress(address); + } + + @override + Future>> getRealRoute( + LatLng myLocation, + LatLng destination, + ) { + return _remoteDataSource.getRealRoute(myLocation, destination); + } + Future> updateOrderState( UpdateOrderStateParams params, ) async { @@ -61,4 +94,13 @@ class OrderDetailsRepoImpl implements OrderDetailsRepo { body: params.body, ); } + + @override + Future updateDriverLocation( + String driverId, + double lat, + double lng, + ) async { + return _remoteDataSource.updateDriverLocation(driverId, lat, lng); + } } diff --git a/lib/features/driver_orders_details/domain/models/drivers_model.dart b/lib/features/driver_orders_details/domain/models/drivers_model.dart new file mode 100644 index 0000000..e8657cd --- /dev/null +++ b/lib/features/driver_orders_details/domain/models/drivers_model.dart @@ -0,0 +1,22 @@ +class DriverDataModel { + final String id; + final String name; + final String phone; + final String deviceToken; + final DriverLocationModel currentLocation; + + DriverDataModel({ + required this.id, + required this.name, + required this.phone, + required this.deviceToken, + required this.currentLocation, + }); +} + +class DriverLocationModel { + final double lat; + final double lng; + + DriverLocationModel({required this.lat, required this.lng}); +} diff --git a/lib/features/driver_orders_details/domain/models/location_type.dart b/lib/features/driver_orders_details/domain/models/location_type.dart new file mode 100644 index 0000000..4572c33 --- /dev/null +++ b/lib/features/driver_orders_details/domain/models/location_type.dart @@ -0,0 +1 @@ +enum LocationType { pickup, user } diff --git a/lib/features/driver_orders_details/domain/repos/order_details_repo.dart b/lib/features/driver_orders_details/domain/repos/order_details_repo.dart index 5eb4574..b53698b 100644 --- a/lib/features/driver_orders_details/domain/repos/order_details_repo.dart +++ b/lib/features/driver_orders_details/domain/repos/order_details_repo.dart @@ -1,11 +1,20 @@ +import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/notcicationModel.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/notficationDevice.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/orderStates.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; abstract class OrderDetailsRepo { - ApiResult> getOrderDetails(String orderId); + Future>> getOrderDetails(); + ApiResult> getDriverData(String driverId); + Future> getLatLngFromAddress(String address); + Future updateDriverLocation(String driverId, double lat, double lng); + Future>> getRealRoute( + LatLng myLocation, + LatLng destination, + ); Future> updateOrderState(UpdateOrderStateParams params); Future> pushNotification(PushNotificationParams params); Future> sendDeviceNotification( diff --git a/lib/features/driver_orders_details/domain/usecases/get_address_usecase.dart b/lib/features/driver_orders_details/domain/usecases/get_address_usecase.dart new file mode 100644 index 0000000..85f8158 --- /dev/null +++ b/lib/features/driver_orders_details/domain/usecases/get_address_usecase.dart @@ -0,0 +1,15 @@ +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/repos/order_details_repo.dart'; + +@injectable +class GetAddressUsecase { + final OrderDetailsRepo _repo; + + GetAddressUsecase(this._repo); + + Future> getAddress(String address) { + return _repo.getLatLngFromAddress(address); + } +} diff --git a/lib/features/driver_orders_details/domain/usecases/get_driver_data_usecase.dart b/lib/features/driver_orders_details/domain/usecases/get_driver_data_usecase.dart new file mode 100644 index 0000000..0680a58 --- /dev/null +++ b/lib/features/driver_orders_details/domain/usecases/get_driver_data_usecase.dart @@ -0,0 +1,13 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/repos/order_details_repo.dart'; + +@injectable +class GetDriverDataUsecase { + OrderDetailsRepo _repo; + GetDriverDataUsecase({required OrderDetailsRepo repo}) : _repo = repo; + + ApiResult> call(String driverId) => + _repo.getDriverData(driverId); +} diff --git a/lib/features/driver_orders_details/domain/usecases/get_order_details_usecase.dart b/lib/features/driver_orders_details/domain/usecases/get_order_details_usecase.dart index e3253c1..37fe21e 100644 --- a/lib/features/driver_orders_details/domain/usecases/get_order_details_usecase.dart +++ b/lib/features/driver_orders_details/domain/usecases/get_order_details_usecase.dart @@ -8,6 +8,5 @@ class GetOrderDetailsUsecase { OrderDetailsRepo _repo; GetOrderDetailsUsecase({required OrderDetailsRepo repo}) : _repo = repo; - ApiResult> call(String orderId) => - _repo.getOrderDetails(orderId); + Future>> call() => _repo.getOrderDetails(); } diff --git a/lib/features/driver_orders_details/domain/usecases/get_real_route_usecase.dart b/lib/features/driver_orders_details/domain/usecases/get_real_route_usecase.dart new file mode 100644 index 0000000..46ee447 --- /dev/null +++ b/lib/features/driver_orders_details/domain/usecases/get_real_route_usecase.dart @@ -0,0 +1,18 @@ +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/repos/order_details_repo.dart'; + +@injectable +class GetRealRouteUsecase { + final OrderDetailsRepo _repo; + + GetRealRouteUsecase(this._repo); + + Future>> getRealRoute( + LatLng driverLocation, + LatLng destination, + ) { + return _repo.getRealRoute(driverLocation, destination); + } +} diff --git a/lib/features/driver_orders_details/domain/usecases/update_driver_location_usecase.dart b/lib/features/driver_orders_details/domain/usecases/update_driver_location_usecase.dart new file mode 100644 index 0000000..70d735d --- /dev/null +++ b/lib/features/driver_orders_details/domain/usecases/update_driver_location_usecase.dart @@ -0,0 +1,13 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/repos/order_details_repo.dart'; + +@injectable +class UpdateDriverLocationUsecase { + final OrderDetailsRepo _repo; + + UpdateDriverLocationUsecase(this._repo); + + Future updateDriverLocation(String driverId, double lat, double lng) { + return _repo.updateDriverLocation(driverId, lat, lng); + } +} diff --git a/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart b/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart index 9d87ae5..a604918 100644 --- a/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart +++ b/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart @@ -1,16 +1,22 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:injectable/injectable.dart'; import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; import 'package:tracking_app/app/config/base_state/base_state.dart'; import 'package:tracking_app/app/config/di/di.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/usecases/get_driver_data_usecase.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/usecases/get_address_usecase.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/notcicationModel.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/notficationDevice.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/orderStates.dart'; -import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/usecases/get_real_route_usecase.dart'; import 'package:tracking_app/features/driver_orders_details/domain/usecases/push_notification_usecase.dart'; import 'package:tracking_app/features/driver_orders_details/domain/usecases/send_device_notification_usecase.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/usecases/update_driver_location_usecase.dart'; import '../../domain/usecases/get_order_details_usecase.dart'; import '../../domain/usecases/update_order_state_usecase.dart'; import 'order_details_intents.dart'; @@ -19,56 +25,163 @@ import 'order_details_states.dart'; @injectable class OrderDetailsCubit extends Cubit { final GetOrderDetailsUsecase _getOrderDetailsUsecase; + final GetDriverDataUsecase _getDriverDataUsecase; final UpdateOrderStateUsecase _updateOrderStateUsecase; final PushNotificationUsecase _pushNotificationUsecase; final SendDeviceNotificationUsecase _sendDeviceNotificationUsecase; - StreamSubscription? _subscription; - final _authStorage = getIt(); + final GetAddressUsecase _getAddressUsecase; + final GetRealRouteUsecase _getRealRouteUsecase; + final UpdateDriverLocationUsecase _updateDriverLocationUsecase; + StreamSubscription? _orderSubscription; + StreamSubscription? _driverSubscription; + Timer? _driverMoveTimer; OrderDetailsCubit( this._getOrderDetailsUsecase, + this._getDriverDataUsecase, + this._getAddressUsecase, + this._getRealRouteUsecase, + this._updateDriverLocationUsecase, this._updateOrderStateUsecase, this._pushNotificationUsecase, this._sendDeviceNotificationUsecase, ) : super(OrderDetailsStates()); + final _authStorage = getIt(); + void onIntent(OrderDetailsIntent intent) { switch (intent) { case GetOrderDetails(): - _getOrderDetails(); + getOrderDetails(); case UpdateOrderState(currentStatus: final status): _updateOrderState(status); } } - void _getOrderDetails() async { + void getOrderDetails() async { emit(state.copyWith(data: Resource.loading())); - _subscription?.cancel(); + _orderSubscription?.cancel(); - try { - final orderId = await _authStorage.getOrderId(); - if (orderId == null || orderId.isEmpty) { - emit(state.copyWith(data: Resource.error('Order ID not found'))); - return; - } + final result = await _getOrderDetailsUsecase.call(); + + if (result is SuccessApiResult>) { + _orderSubscription = result.data.listen( + (order) { + emit(state.copyWith(data: Resource.success(order))); + if (order.driverId.isNotEmpty) { + getDriverData(order.driverId); + } + }, + onError: (error) { + emit(state.copyWith(data: Resource.error(error.toString()))); + }, + ); + } else if (result is ErrorApiResult>) { + emit(state.copyWith(data: Resource.error(result.error))); + } + } + + void getDriverData(String driverId) async { + emit(state.copyWith(driverData: Resource.loading())); + _driverSubscription?.cancel(); + final result = _getDriverDataUsecase.call(driverId); + if (result is SuccessApiResult>) { + _driverSubscription = result.data.listen((driver) async { + emit(state.copyWith(driverData: Resource.success(driver))); + }); + } else if (result is ErrorApiResult>) { + emit(state.copyWith(driverData: Resource.error(result.error))); + } + } + + Future getRoute(LatLng driverLocation) async { + if (state.destination == null) return; + + final result = await _getRealRouteUsecase.getRealRoute( + driverLocation, + state.destination!, + ); + + if (result is SuccessApiResult>) { + emit(state.copyWith(polylines: result.data)); + startDriverSimulation(); + } + } + + Future setDestinationFromAddress( + String address, + LatLng driverLocation, + ) async { + final result = await _getAddressUsecase.getAddress(address); + if (result is SuccessApiResult && result.data != null) { + emit(state.copyWith(destination: result.data)); + startDriverSimulation(); + await getRoute(driverLocation); + } + } + + LatLng moveTowards(LatLng current, LatLng destination, double step) { + double latDiff = destination.latitude - current.latitude; + double lngDiff = destination.longitude - current.longitude; + + double newLat = current.latitude + (latDiff * step); + double newLng = current.longitude + (lngDiff * step); + + return LatLng(newLat, newLng); + } + + void startDriverSimulation() { + _driverMoveTimer?.cancel(); + + _driverMoveTimer = Timer.periodic(const Duration(seconds: 3), ( + timer, + ) async { + final driver = state.driverData?.data; + final destination = state.destination; + + if (driver == null || destination == null) return; + + LatLng currentLocation = LatLng( + driver.currentLocation.lat, + driver.currentLocation.lng, + ); + + final result = await _getRealRouteUsecase.getRealRoute( + currentLocation, + destination, + ); + + if (result is SuccessApiResult>) { + final route = result.data; - final result = _getOrderDetailsUsecase.call(orderId); + if (route.isEmpty) return; - if (result is SuccessApiResult>) { - _subscription = result.data.listen( - (order) => emit(state.copyWith(data: Resource.success(order))), - onError: (error) => - emit(state.copyWith(data: Resource.error(error.toString()))), + final nextPoint = route.length > 2 ? route[1] : route.first; + + await _updateDriverLocationUsecase.updateDriverLocation( + driver.id, + nextPoint.latitude, + nextPoint.longitude, ); - } else if (result is ErrorApiResult>) { - emit(state.copyWith(data: Resource.error(result.error))); + + emit(state.copyWith(polylines: route)); } - } catch (e) { - emit( - state.copyWith( - data: Resource.error('Error retrieving order details: $e'), - ), - ); + }); + } + + String? _nextStateFor(String currentStatus) { + switch (currentStatus.toLowerCase()) { + case 'pending': + case 'accepted': + return 'Picked'; + case 'picked': + return 'Out for delivery'; + case 'out for delivery': + return 'Arrived'; + case 'arrived': + return 'Delivered'; + default: + return null; } } @@ -104,25 +217,11 @@ class OrderDetailsCubit extends Cubit { } } - String? _nextStateFor(String currentStatus) { - switch (currentStatus.toLowerCase()) { - case 'pending': - case 'accepted': - return 'Picked'; - case 'picked': - return 'Out for delivery'; - case 'out for delivery': - return 'Arrived'; - case 'arrived': - return 'Delivered'; - default: - return null; - } - } - @override Future close() { - _subscription?.cancel(); + _orderSubscription?.cancel(); + _driverSubscription?.cancel(); + _driverMoveTimer?.cancel(); return super.close(); } } diff --git a/lib/features/driver_orders_details/presentation/manager/order_details_states.dart b/lib/features/driver_orders_details/presentation/manager/order_details_states.dart index 267a1ca..8adc425 100644 --- a/lib/features/driver_orders_details/presentation/manager/order_details_states.dart +++ b/lib/features/driver_orders_details/presentation/manager/order_details_states.dart @@ -1,11 +1,31 @@ +import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; class OrderDetailsStates { final Resource? data; - const OrderDetailsStates({this.data}); + final Resource? driverData; + final LatLng? destination; + final List? polylines; + const OrderDetailsStates({ + this.data, + this.driverData, + this.destination, + this.polylines, + }); - OrderDetailsStates copyWith({Resource? data}) { - return OrderDetailsStates(data: data ?? this.data); + OrderDetailsStates copyWith({ + Resource? data, + Resource? driverData, + LatLng? destination, + List? polylines, + }) { + return OrderDetailsStates( + data: data ?? this.data, + driverData: driverData ?? this.driverData, + destination: destination ?? this.destination, + polylines: polylines ?? this.polylines, + ); } } diff --git a/lib/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart b/lib/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart index caf7c60..b29b827 100644 --- a/lib/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart +++ b/lib/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart @@ -4,9 +4,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:tracking_app/app/config/base_state/base_state.dart'; import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/app/core/router/route_names.dart'; import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; import 'package:tracking_app/app/core/values/paths.dart'; import 'package:tracking_app/app/core/widgets/custom_button.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/location_type.dart'; import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_cubit.dart'; import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_intents.dart'; import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_states.dart'; @@ -22,151 +24,186 @@ class DriversOrdersDetailsPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios, color: AppColors.blackColor), - onPressed: () => context.pop(), - ), - title: Text( - LocaleKeys.orderDetails.tr(), - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 20, - color: AppColors.blackColor, + final order = getIt().state.data?.data; + final status = OrderStatus.fromString(order?.orderDetails.status); + + return PopScope( + canPop: status == OrderStatus.delivered, + onPopInvoked: (didPop) { + if (!didPop && status != OrderStatus.delivered) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(LocaleKeys.finishYourOrder.tr())), + ); + } + }, + child: Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: AppColors.blackColor), + onPressed: () { + if (status == OrderStatus.delivered) { + context.pop(); + } + }, + ), + title: Text( + LocaleKeys.orderDetails.tr(), + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 20, + color: AppColors.blackColor, + ), ), ), - ), - body: BlocProvider( - create: (context) => - getIt()..onIntent(GetOrderDetails()), - child: BlocBuilder( - builder: (context, state) { - if (state.data?.status == Status.loading) { - return const Center(child: CircularProgressIndicator()); - } else if (state.data?.status == Status.error) { - return Center(child: Text(state.data!.error.toString())); - } else if (state.data?.status == Status.success) { - final order = state.data!.data; - final status = OrderStatus.fromString(order?.orderDetails.status); + body: BlocProvider( + create: (context) => + getIt()..onIntent(GetOrderDetails()), + child: BlocBuilder( + builder: (context, state) { + if (state.data?.status == Status.loading) { + return const Center(child: CircularProgressIndicator()); + } else if (state.data?.status == Status.error) { + return Center(child: Text(state.data!.error.toString())); + } else if (state.data?.status == Status.success) { + final order = state.data!.data; + final status = OrderStatus.fromString( + order?.orderDetails.status, + ); - int currentStep = status.step; - return SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: List.generate(5, (index) { - return Expanded( - child: Container( - height: 4, - margin: const EdgeInsets.symmetric(horizontal: 2), - decoration: BoxDecoration( - color: index < currentStep - ? AppColors.green - : AppColors.lightGrey, - borderRadius: BorderRadius.circular(2), + int currentStep = status.step; + return SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: List.generate(5, (index) { + return Expanded( + child: Container( + height: 4, + margin: const EdgeInsets.symmetric(horizontal: 2), + decoration: BoxDecoration( + color: index < currentStep + ? AppColors.green + : AppColors.lightGrey, + borderRadius: BorderRadius.circular(2), + ), ), - ), - ); - }), - ), - const SizedBox(height: 20), - - Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppColors.lightPink, - borderRadius: BorderRadius.circular(12), + ); + }), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${LocaleKeys.status.tr()}${order?.orderDetails.status}', - style: TextStyle( - color: AppColors.green, - fontWeight: FontWeight.bold, - fontSize: 16, + const SizedBox(height: 20), + + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.lightPink, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${LocaleKeys.status.tr()}${order?.orderDetails.status}', + style: TextStyle( + color: AppColors.green, + fontWeight: FontWeight.bold, + fontSize: 16, + ), ), - ), - const SizedBox(height: 4), - Text( - '${LocaleKeys.orderId.tr()}${order?.orderId}', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, + const SizedBox(height: 4), + Text( + '${LocaleKeys.orderId.tr()}${order?.orderId}', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), ), - ), - const SizedBox(height: 4), - Text( - 'Wed, 03 Sep 2024, 11:00 AM', - style: TextStyle( - color: AppColors.grey, - fontSize: 14, + const SizedBox(height: 4), + Text( + 'Wed, 03 Sep 2024, 11:00 AM', + style: TextStyle( + color: AppColors.grey, + fontSize: 14, + ), ), - ), - ], + ], + ), ), - ), - const SizedBox(height: 24), + const SizedBox(height: 24), - SectionTitle(title: LocaleKeys.pickupAddress.tr()), - AddressCard( - title: order?.orderDetails.pickupAddress.name ?? '', - address: order?.orderDetails.pickupAddress.address ?? '', - imagePath: AppPaths.flowerLogo, - ), - const SizedBox(height: 16), - SectionTitle(title: LocaleKeys.userAddress.tr()), - - AddressCard( - title: order?.userAddress.name ?? '', - address: order?.userAddress.address ?? '', - imagePath: AppPaths.flowerLogo, - ), - const SizedBox(height: 24), + SectionTitle(title: LocaleKeys.pickupAddress.tr()), + InkWell( + onTap: () => context.push( + RouteNames.locationPage, + extra: LocationType.pickup, + ), + child: AddressCard( + title: order?.orderDetails.pickupAddress.name ?? '', + address: + order?.orderDetails.pickupAddress.address ?? '', + imagePath: AppPaths.flowerLogo, + phoneNumber: (state.driverData?.data?.phone) + .toString(), + ), + ), + const SizedBox(height: 16), + SectionTitle(title: LocaleKeys.userAddress.tr()), + InkWell( + onTap: () => context.push( + RouteNames.locationPage, + extra: LocationType.user, + ), + child: AddressCard( + title: order?.userAddress.name ?? '', + address: order?.userAddress.address ?? '', + imagePath: AppPaths.flowerLogo, + phoneNumber: (state.driverData?.data?.phone) + .toString(), + ), + ), + const SizedBox(height: 24), - SectionTitle(title: LocaleKeys.orderDetails.tr()), - OrderItems(), - const SizedBox(height: 16), + SectionTitle(title: LocaleKeys.orderDetails.tr()), + OrderItems(), + const SizedBox(height: 16), - BottomRowSection( - label: LocaleKeys.total.tr(), - value: - '${LocaleKeys.egp.tr()} ${order?.orderDetails.totalPrice.toStringAsFixed(2)}', - ), - BottomRowSection( - label: LocaleKeys.payment_method.tr(), - value: LocaleKeys.cash_on_delivery.tr(), - ), + BottomRowSection( + label: LocaleKeys.total.tr(), + value: + '${LocaleKeys.egp.tr()} ${order?.orderDetails.totalPrice.toStringAsFixed(2)}', + ), + BottomRowSection( + label: LocaleKeys.payment_method.tr(), + value: LocaleKeys.cash_on_delivery.tr(), + ), - const SizedBox(height: 32), + const SizedBox(height: 32), - SizedBox( - width: double.infinity, - height: 55, - child: CustomButton( - isEnabled: status != OrderStatus.delivered, - onPressed: () { - if (status != OrderStatus.delivered && - order != null) { - context.read().onIntent( - UpdateOrderState(order.orderDetails.status), - ); - } - }, - isLoading: false, - text: status.buttonTextKey.tr(), + SizedBox( + width: double.infinity, + height: 55, + child: CustomButton( + isEnabled: status != OrderStatus.delivered, + onPressed: () { + if (status != OrderStatus.delivered && + order != null) { + context.read().onIntent( + UpdateOrderState(order.orderDetails.status), + ); + } + }, + isLoading: false, + text: status.buttonTextKey.tr(), + ), ), - ), - ], - ), - ); - } - return const SizedBox.shrink(); - }, + ], + ), + ); + } + return const SizedBox.shrink(); + }, + ), ), ), ); diff --git a/lib/features/driver_orders_details/presentation/pages/location_page.dart b/lib/features/driver_orders_details/presentation/pages/location_page.dart new file mode 100644 index 0000000..93cf845 --- /dev/null +++ b/lib/features/driver_orders_details/presentation/pages/location_page.dart @@ -0,0 +1,261 @@ +import 'dart:typed_data'; +import 'package:flutter/services.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/app/core/ui_helper/assets/images.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/app/core/values/paths.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/location_type.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_cubit.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_states.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/widgets/address_card.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/widgets/section_title.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; + +class LocationPage extends StatefulWidget { + final LocationType locationType; + const LocationPage({super.key, required this.locationType}); + + @override + State createState() => _LocationPageState(); +} + +class _LocationPageState extends State { + late OrderDetailsCubit cubit; + + LatLng? destination; + Set polylines = {}; + + Set markers = {}; + BitmapDescriptor? driverIcon; + BitmapDescriptor? destinationIcon; + + @override + void initState() { + super.initState(); + cubit = getIt(); + cubit.getOrderDetails(); + loadMarkerIcons(); + } + + Future getMarkerIcon(String path) async { + final ByteData data = await DefaultAssetBundle.of(context).load(path); + final Uint8List bytes = data.buffer.asUint8List(); + return BitmapDescriptor.fromBytes(bytes); + } + + Future loadMarkerIcons() async { + driverIcon = await getMarkerIcon(Assets.driverLocation); + + destinationIcon = await getMarkerIcon( + widget.locationType == LocationType.pickup + ? Assets.floweryLocation + : Assets.userLocation, + ); + setState(() {}); + } + + void driverMarker(LatLng driverLocation) { + markers.add( + Marker( + markerId: const MarkerId("driver_location"), + position: driverLocation, + icon: driverIcon ?? BitmapDescriptor.defaultMarker, + infoWindow: const InfoWindow(title: "Your location"), + ), + ); + } + + void destinationMarker(LatLng destinationLocation) { + markers.add( + Marker( + markerId: const MarkerId("destination_location"), + position: destinationLocation, + icon: destinationIcon ?? BitmapDescriptor.defaultMarker, + infoWindow: const InfoWindow(title: "Destination"), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: BlocProvider( + create: (context) => cubit, + child: BlocConsumer( + listener: (context, state) { + final driver = state.driverData?.data; + final order = state.data?.data; + if (driver == null || order == null) return; + + final driverLocation = LatLng( + driver.currentLocation.lat, + driver.currentLocation.lng, + ); + String address; + + if (widget.locationType == LocationType.pickup) { + address = order.orderDetails.pickupAddress.address; + } else { + address = order.userAddress.address; + } + + print( + '<<<<<<< driver $driver, order $order, ${state.destination}, ${state.polylines}', + ); + + cubit.setDestinationFromAddress(address, driverLocation); + + driverMarker(driverLocation); + + if (state.destination == null || state.polylines == null) return; + destinationMarker(state.destination!); + + if (state.polylines != null) { + polylines = { + Polyline( + polylineId: const PolylineId("real_route"), + color: AppColors.pink, + width: 5, + points: state.polylines ?? [], + ), + }; + } + setState(() {}); + }, + + builder: (context, state) { + final driver = state.driverData?.data; + if (driver == null) { + return const Center(child: CircularProgressIndicator()); + } + + final driverLocation = LatLng( + driver.currentLocation.lat, + driver.currentLocation.lng, + ); + + return Stack( + alignment: Alignment.topLeft, + + children: [ + Column( + children: [ + Expanded( + child: GoogleMap( + initialCameraPosition: CameraPosition( + target: driverLocation, + zoom: 18, + ), + mapType: MapType.normal, + markers: markers, + polylines: polylines, + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.locationType == LocationType.pickup) ...[ + SectionTitle(title: LocaleKeys.pickupAddress.tr()), + AddressCard( + title: + state + .data + ?.data + ?.orderDetails + .pickupAddress + .name ?? + '', + address: + state + .data + ?.data + ?.orderDetails + .pickupAddress + .address ?? + '', + imagePath: AppPaths.flowerLogo, + phoneNumber: (state.driverData?.data?.phone) + .toString(), + ), + const SizedBox(height: 16), + SectionTitle(title: LocaleKeys.userAddress.tr()), + + AddressCard( + title: state.data?.data?.userAddress.name ?? '', + address: + state.data?.data?.userAddress.address ?? '', + imagePath: AppPaths.flowerLogo, + phoneNumber: (state.driverData?.data?.phone) + .toString(), + ), + ] else ...[ + SectionTitle(title: LocaleKeys.userAddress.tr()), + AddressCard( + title: state.data?.data?.userAddress.name ?? '', + address: + state.data?.data?.userAddress.address ?? '', + imagePath: AppPaths.flowerLogo, + phoneNumber: (state.driverData?.data?.phone) + .toString(), + ), + const SizedBox(height: 16), + SectionTitle(title: LocaleKeys.pickupAddress.tr()), + AddressCard( + title: + state + .data + ?.data + ?.orderDetails + .pickupAddress + .name ?? + '', + address: + state + .data + ?.data + ?.orderDetails + .pickupAddress + .address ?? + '', + imagePath: AppPaths.flowerLogo, + phoneNumber: (state.driverData?.data?.phone) + .toString(), + ), + ], + ], + ), + ), + ], + ), + + Positioned( + top: 40, + left: 16, + child: InkWell( + onTap: () => context.pop(), + child: CircleAvatar( + backgroundColor: AppColors.pink, + child: Center( + child: Icon( + Icons.arrow_back_ios_new, + color: AppColors.white, + ), + ), + ), + ), + ), + ], + ); + }, + ), + ), + ); + } +} diff --git a/lib/features/driver_orders_details/presentation/widgets/address_card.dart b/lib/features/driver_orders_details/presentation/widgets/address_card.dart index 6b211c2..0cfca02 100644 --- a/lib/features/driver_orders_details/presentation/widgets/address_card.dart +++ b/lib/features/driver_orders_details/presentation/widgets/address_card.dart @@ -1,17 +1,20 @@ import 'package:flutter/material.dart'; import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/app/core/utils/app_launcher.dart'; import 'package:tracking_app/app/core/values/paths.dart'; class AddressCard extends StatelessWidget { final String title; final String address; final String imagePath; + final String phoneNumber; const AddressCard({ super.key, required this.title, required this.address, required this.imagePath, + required this.phoneNumber, }); @override @@ -60,12 +63,12 @@ class AddressCard extends StatelessWidget { ), ), IconButton( - onPressed: () {}, + onPressed: () => AppLauncher.launchPhone(phoneNumber), icon: Icon(Icons.phone_outlined, color: AppColors.pink, size: 20), ), IconButton( - onPressed: () {}, + onPressed: () => AppLauncher.launchWhatsApp(phoneNumber), icon: ImageIcon( AssetImage(AppPaths.whatsappImage), color: AppColors.pink, diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index a5dd194..d0907a7 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -269,8 +269,9 @@ abstract class LocaleKeys { static const reject = 'reject'; static const noPendingOrders = 'noPendingOrders'; static const floweryRider = 'floweryRider'; - static const btnArrivedAtPickupPoint = 'ArrivedAtPickupPoint'; - static const btnStartDeliver = 'StartDeliver'; - static const btnArrivedToUser = 'ArrivedToUser'; - static const btnDeliveredToUser = 'DeliveredToUser'; + static const btnArrivedAtPickupPoint = 'btnArrivedAtPickupPoint'; + static const btnStartDeliver = 'btnStartDeliver'; + static const btnArrivedToUser = 'btnArrivedToUser'; + static const btnDeliveredToUser = 'btnDeliveredToUser'; + static const finishYourOrder = 'finishYourOrder'; } diff --git a/pubspec.lock b/pubspec.lock index ab1b97b..917619d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -563,6 +563,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.33" + flutter_polyline_points: + dependency: "direct main" + description: + name: flutter_polyline_points + sha256: c775fe59fbcf1f925d611c039555c7f58ed6d9411747b7a2915bbd9c5e730a51 + url: "https://pub.dev" + source: hosted + version: "3.1.0" flutter_svg: dependency: "direct main" description: @@ -993,10 +1001,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.16.0" mime: dependency: transitive description: @@ -1502,26 +1510,26 @@ packages: dependency: transitive description: name: test - sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" url: "https://pub.dev" source: hosted - version: "1.26.3" + version: "1.26.2" test_api: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.6" test_core: dependency: transitive description: name: test_core - sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" url: "https://pub.dev" source: hosted - version: "0.6.12" + version: "0.6.11" timezone: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1232424..fa792b5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,7 +38,8 @@ dependencies: flutter_local_notifications: ^20.0.0 firebase_crashlytics: ^5.0.7 cloud_firestore: ^6.1.2 - cached_network_image: ^3.3.1 + cached_network_image: ^3.3.1 + flutter_polyline_points: ^3.1.0 googleapis_auth: ^2.0.0 dev_dependencies: diff --git a/test/app/config/auth_storage/auth_storage_test.dart b/test/app/config/auth_storage/auth_storage_test.dart new file mode 100644 index 0000000..187c7bf --- /dev/null +++ b/test/app/config/auth_storage/auth_storage_test.dart @@ -0,0 +1,69 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; + +void main() { + late AuthStorage authStorage; + + setUp(() { + authStorage = AuthStorage(); + SharedPreferences.setMockInitialValues({}); + }); + + group('AuthStorage Tests', () { + test( + 'saveToken should call setString with correct key and value', + () async { + const token = 'test_token_123'; + + await authStorage.saveToken(token); + + final storedToken = await authStorage.getToken(); + expect(storedToken, token); + }, + ); + + test('getRememberMe should return false by default if not set', () async { + final result = await authStorage.getRememberMe(); + + expect(result, false); + }); + + test('setRememberMe should store boolean value correctly', () async { + await authStorage.setRememberMe(true); + + final result = await authStorage.getRememberMe(); + expect(result, true); + }); + + test('saveUserJson and getUserJson should handle string data', () async { + const userJson = '{"id": 1, "name": "Gemini"}'; + + await authStorage.saveUserJson(userJson); + final result = await authStorage.getUserJson(); + + expect(result, userJson); + }); + + test('clearOrderId should remove the order id from prefs', () async { + await authStorage.saveOrderId('order_999'); + + await authStorage.clearOrderId(); + final result = await authStorage.getOrderId(); + + expect(result, null); + }); + + test('clearAll should reset all stored values', () async { + await authStorage.saveToken('token'); + await authStorage.setRememberMe(true); + await authStorage.saveOrderId('123'); + + await authStorage.clearAll(); + + expect(await authStorage.getToken(), null); + expect(await authStorage.getRememberMe(), false); + expect(await authStorage.getOrderId(), null); + }); + }); +} diff --git a/test/app/config/validation/app_validation_test.dart b/test/app/config/validation/app_validation_test.dart new file mode 100644 index 0000000..b8f6a47 --- /dev/null +++ b/test/app/config/validation/app_validation_test.dart @@ -0,0 +1,149 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tracking_app/app/config/validation/app_validation.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; + +void main() { + setUpAll(() async { + SharedPreferences.setMockInitialValues({}); + EasyLocalization.logger.enableLevels = []; + }); + + group('Validators Unit Tests', () { + group('firstNameValidator', () { + test('should return required error for null or empty', () { + expect( + Validators.firstNameValidator(null), + LocaleKeys.firstNameRequired.tr(), + ); + expect( + Validators.firstNameValidator(''), + LocaleKeys.firstNameRequired.tr(), + ); + }); + + test( + 'should return invalid error for names < 3 or > 50 or with numbers', + () { + expect( + Validators.firstNameValidator('Ab'), + LocaleKeys.nameInvalid.tr(), + ); + expect( + Validators.firstNameValidator('Ab12'), + LocaleKeys.nameInvalid.tr(), + ); + }, + ); + + test('should return null for valid first name', () { + expect(Validators.firstNameValidator('Ahmed'), null); + }); + }); + + group('phoneValidator', () { + test('should return required error for empty phone', () { + expect(Validators.phoneValidator(''), LocaleKeys.phoneRequired.tr()); + }); + + test('should return invalid for non-Egyptian format or wrong length', () { + // Regex بيطلب يبدأ بـ +201 وبعدها [0-2, 5] وبعدها 8 أرقام + expect( + Validators.phoneValidator('01012345678'), + LocaleKeys.phoneInvalid.tr(), + ); // ناقص الـ +20 + expect( + Validators.phoneValidator('+201312345678'), + LocaleKeys.phoneInvalid.tr(), + ); // رقم 3 مش موجود في الـ range + }); + + test('should return null for valid Egyptian phone (+2010...)', () { + expect(Validators.phoneValidator('+201012345678'), null); + }); + }); + + group('passwordValidator', () { + test( + 'should validate length, capital, small, number, and special char', + () { + expect( + Validators.passwordValidator(''), + LocaleKeys.passwordRequired.tr(), + ); + expect( + Validators.passwordValidator('123ab'), + LocaleKeys.passwordLengthInvalid.tr(), + ); + expect( + Validators.passwordValidator('abcdef123!'), + LocaleKeys.passwordUpperLetterInvalid.tr(), + ); + expect( + Validators.passwordValidator('ABCDEF123!'), + LocaleKeys.passwordLowerLetterInvalid.tr(), + ); + expect( + Validators.passwordValidator('Abcdefgh!'), + LocaleKeys.passwordNumbersInvalid.tr(), + ); + expect( + Validators.passwordValidator('Abcdefgh1'), + LocaleKeys.passwordSpecialCharInvalid.tr(), + ); + }, + ); + + test('should return null for strong password', () { + expect(Validators.passwordValidator('Strong123!'), null); + }); + }); + + group('newPasswordValidator', () { + test('should return error if same as current password', () { + const currentPass = 'OldPass123!'; + expect( + Validators.newPasswordValidator(currentPass, currentPass), + LocaleKeys.cannotBeSame.tr(), + ); + }); + }); + + group('confirmPasswordValidator', () { + test('should return error if passwords do not match', () { + expect( + Validators.confirmPasswordValidator('Pass1', 'Pass2'), + LocaleKeys.passwordsDoNotMatch.tr(), + ); + }); + }); + + group('emailValidator', () { + test('should return invalid for wrong email formats', () { + expect( + Validators.emailValidator('test@'), + LocaleKeys.emailInvalid.tr(), + ); + expect( + Validators.emailValidator('test@domain'), + LocaleKeys.emailInvalid.tr(), + ); + }); + + test('should return null for valid email', () { + expect(Validators.emailValidator('user@example.com'), null); + }); + }); + + group('genderValidator', () { + test('should return error if gender is not selected', () { + expect( + Validators.genderValidator(null), + LocaleKeys.genderRequired.tr(), + ); + expect(Validators.genderValidator(''), LocaleKeys.genderRequired.tr()); + }); + }); + }); +} diff --git a/test/app/core/ui_helper/style/font_style_test.dart b/test/app/core/ui_helper/style/font_style_test.dart new file mode 100644 index 0000000..559dae4 --- /dev/null +++ b/test/app/core/ui_helper/style/font_style_test.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/app/core/ui_helper/style/font_style.dart'; + +void main() { + group('AppStyles Tests', () { + test('font32BlackSemiBold should have correct properties', () { + final style = AppStyles.font32BlackSemiBold; + + expect(style.fontSize, 32); + expect(style.color, AppColors.blackColor); + expect(style.fontWeight, FontWeight.w500); + expect(style.fontFamily, 'SansArabic'); + }); + + test('subtitle should have correct properties', () { + final style = AppStyles.subtitle; + + expect(style.fontSize, 12); + expect(style.color, AppColors.grey); + expect(style.fontWeight, FontWeight.normal); + }); + + test('All styles should use the correct default fontFamily', () { + expect(AppStyles.black24SemiBold.fontFamily, 'SansArabic'); + expect(AppStyles.font16Black.fontFamily, 'SansArabic'); + expect(AppStyles.purple18bold.fontFamily, 'SansArabic'); + }); + + test('Special case: medium20 should use Inter font', () { + expect(AppStyles.medium20.fontFamily, 'Inter'); + expect(AppStyles.medium20.fontSize, 20); + }); + + test('red14Normal should return red color from AppColors', () { + expect(AppStyles.red14Normal.color, AppColors.red); + expect(AppStyles.red14Normal.fontSize, 14); + }); + + test('Check for specific bug: font12White color fix', () { + expect(AppStyles.font12White.color, AppColors.blackColor); + }); + }); +} diff --git a/test/app/core/utils/app_launcher_test.dart b/test/app/core/utils/app_launcher_test.dart new file mode 100644 index 0000000..a17a87b --- /dev/null +++ b/test/app/core/utils/app_launcher_test.dart @@ -0,0 +1,69 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:tracking_app/app/core/utils/app_launcher.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; +import 'app_launcher_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(mixingIn: [MockPlatformInterfaceMixin]), +]) +void main() { + late MockUrlLauncherPlatform mockPlatform; + + setUp(() { + mockPlatform = MockUrlLauncherPlatform(); + UrlLauncherPlatform.instance = mockPlatform; + }); + + group('AppLauncher Tests', () { + test('launchPhone should call launchUrl with tel scheme', () async { + const phoneNumber = '0123456789'; + const expectedUrl = 'tel:0123456789'; + + when(mockPlatform.canLaunch(expectedUrl)).thenAnswer((_) async => true); + when( + mockPlatform.launchUrl(expectedUrl, any), + ).thenAnswer((_) async => true); + + AppLauncher.launchPhone(phoneNumber); + + await untilCalled(mockPlatform.launchUrl(expectedUrl, any)); + + verify(mockPlatform.launchUrl(expectedUrl, any)).called(1); + }); + + test( + 'launchWhatsApp should format Egyptian numbers correctly and launch', + () async { + const phoneNumber = '01012345678'; + const expectedUrl = 'whatsapp://send?phone=201012345678'; + + when( + mockPlatform.launchUrl(expectedUrl, any), + ).thenAnswer((_) async => true); + + AppLauncher.launchWhatsApp(phoneNumber); + + await untilCalled(mockPlatform.launchUrl(expectedUrl, any)); + + verify(mockPlatform.launchUrl(expectedUrl, any)).called(1); + }, + ); + + test('launchWhatsApp should strip non-numeric characters', () async { + const phoneNumber = '+20 (123) 456-789'; + const expectedUrl = 'whatsapp://send?phone=20123456789'; + + when( + mockPlatform.launchUrl(expectedUrl, any), + ).thenAnswer((_) async => true); + + AppLauncher.launchWhatsApp(phoneNumber); + await untilCalled(mockPlatform.launchUrl(expectedUrl, any)); + + verify(mockPlatform.launchUrl(expectedUrl, any)).called(1); + }); + }); +} diff --git a/test/app/core/utils/validators_helper_test.dart b/test/app/core/utils/validators_helper_test.dart new file mode 100644 index 0000000..595ca7e --- /dev/null +++ b/test/app/core/utils/validators_helper_test.dart @@ -0,0 +1,146 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/app/core/utils/validators_helper.dart'; +import 'package:tracking_app/app/core/values/user_error_mesagges.dart'; + +void main() { + group('Validators Tests', () { + group('validateEmail', () { + test('should return error if email is empty', () { + expect(Validators.validateEmail(''), UserErrorMessages.emailRequired); + expect(Validators.validateEmail(null), UserErrorMessages.emailRequired); + }); + + test('should return error if email format is invalid', () { + expect( + Validators.validateEmail('test'), + UserErrorMessages.invalidEmail, + ); + expect( + Validators.validateEmail('test@'), + UserErrorMessages.invalidEmail, + ); + expect( + Validators.validateEmail('test@domain'), + UserErrorMessages.invalidEmail, + ); + }); + + test('should return null if email is valid', () { + expect(Validators.validateEmail('test@example.com'), null); + }); + }); + + group('validatePassword', () { + test('should return error if password is empty', () { + expect( + Validators.validatePassword(''), + UserErrorMessages.passwordRequired, + ); + }); + + test('should return error if password < 6 characters', () { + expect( + Validators.validatePassword('Ab1'), + UserErrorMessages.least6Characters, + ); + }); + + test('should return error if no capital letter', () { + expect( + Validators.validatePassword('abc12345'), + UserErrorMessages.passwordWithCapital, + ); + }); + + test('should return error if no number', () { + expect( + Validators.validatePassword('Abcdefgh'), + UserErrorMessages.passwordWithNumber, + ); + }); + + test('should return null if password is valid', () { + expect(Validators.validatePassword('Password123'), null); + }); + }); + + group('validateRePassword', () { + test('should return error if confirm password is empty', () { + expect( + Validators.validateRePassword('', 'Password123'), + UserErrorMessages.confirmPassword, + ); + }); + + test('should return error if passwords do not match', () { + expect( + Validators.validateRePassword('123', '456'), + UserErrorMessages.passwordDontMatch, + ); + }); + + test('should return null if passwords match', () { + expect(Validators.validateRePassword('Pass123', 'Pass123'), null); + }); + }); + + group('validatePhone', () { + test('should return error if phone is empty', () { + expect(Validators.validatePhone(''), UserErrorMessages.phoneRequired); + }); + + test( + 'should return error if phone format is invalid (Egyptian format)', + () { + expect( + Validators.validatePhone('12345678901'), + UserErrorMessages.invalidNumber, + ); // No 01 at start + expect( + Validators.validatePhone('0101234567'), + UserErrorMessages.invalidNumber, + ); // Too short + }, + ); + + test('should return null if phone is valid', () { + expect(Validators.validatePhone('01012345678'), null); + expect(Validators.validatePhone('01112345678'), null); + }); + }); + + group('validateName / RecipientName / Address', () { + test('validateName should catch special characters and length', () { + expect( + Validators.validateName('ab'), + UserErrorMessages.least3Characters, + ); + expect(Validators.validateName('John@'), UserErrorMessages.invalidName); + expect(Validators.validateName('John Doe'), null); + }); + + test('validateRecipientName should return specific error messages', () { + expect( + Validators.validateRecipientName(''), + UserErrorMessages.requiredRecipientName, + ); + expect( + Validators.validateRecipientName('Al!'), + UserErrorMessages.invalidRecipientName, + ); + }); + + test('validateAddress should return specific error messages', () { + expect( + Validators.validateAddress(''), + UserErrorMessages.requiredAddress, + ); + expect( + Validators.validateAddress('Cairo#5'), + UserErrorMessages.invalidAddress, + ); + expect(Validators.validateAddress('Maadi, Cairo'), null); + }); + }); + }); +} diff --git a/test/features/auth/data/mappers/change_password_dto_mapper_test.dart b/test/features/auth/data/mapper/change_password_dto_mapper_test.dart similarity index 88% rename from test/features/auth/data/mappers/change_password_dto_mapper_test.dart rename to test/features/auth/data/mapper/change_password_dto_mapper_test.dart index a673111..167ca64 100644 --- a/test/features/auth/data/mappers/change_password_dto_mapper_test.dart +++ b/test/features/auth/data/mapper/change_password_dto_mapper_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:tracking_app/features/auth/data/mappers/change_password_dto_mapper.dart'; +import 'package:tracking_app/features/auth/data/mapper/change_password_dto_mapper.dart'; import 'package:tracking_app/features/auth/data/model/response/change_password_dto.dart'; import 'package:tracking_app/features/auth/domain/models/change_password_model.dart'; diff --git a/test/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl_test.dart b/test/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl_test.dart index 35c60c2..be1433a 100644 --- a/test/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl_test.dart +++ b/test/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl_test.dart @@ -1,10 +1,12 @@ +import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:dio/dio.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/drivers_dto.dart'; import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; import 'order_details_remote_datasource_impl_test.mocks.dart'; @@ -18,15 +20,17 @@ import 'order_details_remote_datasource_impl_test.mocks.dart'; void main() { late OrderDetailsRemoteDatasourceImpl dataSource; late MockFirebaseFirestore mockFirestore; + late MockDio mockDio; late MockCollectionReference> mockCollection; late MockDocumentReference> mockDocument; late MockDocumentSnapshot> mockSnapshot; - late MockDio mockDio; const String tOrderId = 'pxkMaEmWYVuvV5jkW0JK'; + const String driverId = '6989f35de364ef61405211a0'; setUp(() { mockFirestore = MockFirebaseFirestore(); + mockDio = MockDio(); mockCollection = MockCollectionReference(); mockDocument = MockDocumentReference(); mockSnapshot = MockDocumentSnapshot(); @@ -78,4 +82,102 @@ void main() { ); }); }); + + group('getDriversData', () { + final driverData = { + 'id': '6989f35de364ef61405211a0', + 'currentLocation': {'lat': 31.251555, 'lng': 29.9843417}, + 'name': "mariam", + 'phone': '01205708282', + 'deviceToken': '', + }; + + test('should return SuccessApiResult with Stream of DriverDto', () async { + when(mockFirestore.collection('drivers')).thenReturn(mockCollection); + when(mockCollection.doc(driverId)).thenReturn(mockDocument); + + when(mockSnapshot.exists).thenReturn(true); + when(mockSnapshot.data()).thenReturn(driverData); + when(mockSnapshot.id).thenReturn(driverId); + + when( + mockDocument.snapshots(), + ).thenAnswer((_) => Stream.value(mockSnapshot)); + + final result = dataSource.getDriverData(driverId); + + expect(result, isA>>()); + final stream = (result as SuccessApiResult>).data; + await expectLater( + stream, + emits( + isA() + .having((o) => o.name, 'name', 'mariam') + .having((o) => o.id, 'id', driverId), + ), + ); + }); + }); + + group('getLatLngFromAddress', () { + test('should return LatLng when API responds with valid data', () async { + final responseData = [ + {"lat": "30.0444", "lon": "31.2357"}, + ]; + + when( + mockDio.get( + any, + queryParameters: anyNamed('queryParameters'), + options: anyNamed('options'), + ), + ).thenAnswer( + (_) async => Response( + data: responseData, + statusCode: 200, + requestOptions: RequestOptions(path: ''), + ), + ); + + final result = await dataSource.getLatLngFromAddress("Cairo"); + + expect(result, isA>()); + final success = result as SuccessApiResult; + expect(success.data!.latitude, 30.0444); + expect(success.data!.longitude, 31.2357); + }); + }); + + group('getRealRoute', () { + test( + 'should return List when API responds with valid route', + () async { + final responseData = { + "code": "Ok", + "routes": [ + {"geometry": "}_ilFjk~uO??"}, + ], + }; + + when( + mockDio.get(any, queryParameters: anyNamed('queryParameters')), + ).thenAnswer( + (_) async => Response( + data: responseData, + statusCode: 200, + requestOptions: RequestOptions(path: ''), + ), + ); + + final result = await dataSource.getRealRoute( + const LatLng(30.0444, 31.2357), + const LatLng(30.0500, 31.2400), + ); + + expect(result, isA>>()); + final success = result as SuccessApiResult>; + expect(success.data, isNotEmpty); + }, + ); + }); } diff --git a/test/features/driver_orders_details/data/mapper/drivers_dto_mapper_test.dart b/test/features/driver_orders_details/data/mapper/drivers_dto_mapper_test.dart new file mode 100644 index 0000000..e4460b4 --- /dev/null +++ b/test/features/driver_orders_details/data/mapper/drivers_dto_mapper_test.dart @@ -0,0 +1,38 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/driver_orders_details/data/mapper/drivers_dto_mapper.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/drivers_dto.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; + +void main() { + group('DriverDataDtoMapper', () { + test('Convert DriverDataDto to DriverDataModel correctly', () { + final dto = DriverDataDto( + deviceToken: 'token', + id: '111', + phone: '0', + currentLocation: DriverLocationDto(lat: 31, lng: 29), + name: 'Mariam', + ); + + final result = dto.toDriversModel(); + + expect(result, isA()); + expect(result.deviceToken, dto.deviceToken); + expect(result.name, dto.name); + expect(result.phone, dto.phone); + expect(result.currentLocation.lat, dto.currentLocation.lat); + }); + }); + + group('DriverLocationDtoMapper', () { + test('Convert DriverLocationDto to DriverLocationModel correctly', () { + final dto = DriverLocationDto(lat: 30, lng: 29); + + final result = dto.toDriverLocationModel(); + + expect(result, isA()); + expect(result.lat, dto.lat); + expect(result.lng, dto.lng); + }); + }); +} diff --git a/test/features/driver_orders_details/data/models/drivers_dto_test.dart b/test/features/driver_orders_details/data/models/drivers_dto_test.dart new file mode 100644 index 0000000..006150e --- /dev/null +++ b/test/features/driver_orders_details/data/models/drivers_dto_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/drivers_dto.dart'; + +void main() { + group('DriverDataDto Tests', () { + test('should return a valid DriverDataDto from JSON', () { + final Map json = { + 'id': '6989f35de364ef61405211a0', + 'currentLocation': {'lat': 31.251555, 'lng': 29.9843417}, + 'name': "mariam", + 'phone': '01205708282', + 'deviceToken': '', + }; + + final result = DriverDataDto.fromJson(json); + + expect(result.phone, '01205708282'); + expect(result.name, 'mariam'); + expect(result.id, '6989f35de364ef61405211a0'); + expect(result.currentLocation.lat, 31.251555); + }); + + test('should return a valid JSON map from DriverDataDto', () { + final dto = DriverDataDto( + currentLocation: DriverLocationDto(lat: 30, lng: 29), + deviceToken: 'token', + id: '123', + phone: '01205708282', + name: 'Mariam', + ); + + final result = dto.toJson(); + + expect(result['deviceToken'], 'token'); + expect(result['name'], 'Mariam'); + expect(result['id'], '123'); + }); + }); + + group('DriverLocationDto Tests', () { + test('should return a valid DriverLocationDto from JSON', () { + final Map json = {'lat': 30, 'lng': 29}; + + final result = DriverLocationDto.fromJson(json); + + expect(result.lat, 30); + expect(result.lng, 29); + }); + + test('should return a valid JSON map from DriverLocationDto', () { + final dto = DriverLocationDto(lat: 30, lng: 29); + + final result = dto.toJson(); + + expect(result['lat'], 30); + expect(result['lng'], 29); + }); + }); +} diff --git a/test/features/driver_orders_details/data/repos/order_details_repo_impl_test.dart b/test/features/driver_orders_details/data/repos/order_details_repo_impl_test.dart index b10ab3e..b21d092 100644 --- a/test/features/driver_orders_details/data/repos/order_details_repo_impl_test.dart +++ b/test/features/driver_orders_details/data/repos/order_details_repo_impl_test.dart @@ -1,28 +1,40 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/drivers_dto.dart'; import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; import 'package:tracking_app/features/driver_orders_details/data/repos/order_details_repo_impl.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; import 'order_details_repo_impl_test.mocks.dart'; -@GenerateMocks([OrderDetailsRemoteDatasource, DocumentSnapshot]) +@GenerateMocks([OrderDetailsRemoteDatasource, DocumentSnapshot, AuthStorage]) void main() { late OrderDetailsRepoImpl repository; + late MockAuthStorage authStorage; late MockOrderDetailsRemoteDatasource mockRemoteDataSource; setUp(() { mockRemoteDataSource = MockOrderDetailsRemoteDatasource(); - repository = OrderDetailsRepoImpl(mockRemoteDataSource); + authStorage = MockAuthStorage(); + repository = OrderDetailsRepoImpl(mockRemoteDataSource, authStorage); provideDummy>>( ErrorApiResult(error: 'dummy_error'), ); + provideDummy>>( + ErrorApiResult(error: 'dummy_error'), + ); + provideDummy>(ErrorApiResult(error: 'dummy_error')); + provideDummy>>(ErrorApiResult(error: 'dummy_error')); }); const tOrderId = 'pxkMaEmWYVuvV5jkW0JK'; + const driverId = '6989f35de364ef61405211a0'; final tOrderDto = OrderDto( driverId: 'D123', @@ -43,15 +55,25 @@ void main() { ), ); + final driverDto = DriverDataDto( + deviceToken: 'token', + id: '6989f35de364ef61405211a0', + name: 'mariam', + phone: '01205708282', + currentLocation: DriverLocationDto(lat: 30, lng: 29), + ); + group('getOrderDetails', () { test( 'should emit OrderModel when the remote data source returns SuccessApiResult with Stream', () async { + when(authStorage.getOrderId()).thenAnswer((_) async => tOrderId); + when( mockRemoteDataSource.getOrderStream(tOrderId), ).thenReturn(SuccessApiResult(data: Stream.value(tOrderDto))); - final result = repository.getOrderDetails(tOrderId); + final result = await repository.getOrderDetails(); expect(result, isA>>()); final stream = (result as SuccessApiResult>).data; @@ -76,15 +98,126 @@ void main() { 'should throw an Exception when the document does not exist', () async { const errorMessage = "Network Error"; + when(authStorage.getOrderId()).thenAnswer((_) async => tOrderId); + when( mockRemoteDataSource.getOrderStream(tOrderId), ).thenReturn(ErrorApiResult(error: errorMessage)); - final result = repository.getOrderDetails(tOrderId); + final result = await repository.getOrderDetails(); expect(result, isA>>()); expect((result as ErrorApiResult).error, errorMessage); }, ); }); + + group('getDriverData', () { + test( + 'should emit DriverDataModel when the remote data source returns SuccessApiResult with Stream', + () async { + when( + mockRemoteDataSource.getDriverData(driverId), + ).thenReturn(SuccessApiResult(data: Stream.value(driverDto))); + + final result = repository.getDriverData(driverId); + + expect(result, isA>>()); + final stream = + (result as SuccessApiResult>).data; + await expectLater( + stream, + emits( + isA() + .having((o) => o.id, 'driver id', driverId) + .having((o) => o.name, 'user name', driverDto.name) + .having((o) => o.currentLocation.lat, 'lat', 30), + ), + ); + }, + ); + + test( + 'should throw an Exception when the document does not exist', + () async { + const errorMessage = "Network Error"; + when( + mockRemoteDataSource.getDriverData(driverId), + ).thenReturn(ErrorApiResult(error: errorMessage)); + + final result = repository.getDriverData(driverId); + + expect(result, isA>>()); + expect((result as ErrorApiResult).error, errorMessage); + }, + ); + }); + group('getLatLngFromAddress', () { + final tAddress = "Cairo"; + final tLatLng = LatLng(30.0, 31.0); + + test( + 'should return SuccessApiResult when remote data source succeeds', + () async { + when( + mockRemoteDataSource.getLatLngFromAddress(tAddress), + ).thenAnswer((_) async => SuccessApiResult(data: tLatLng)); + + final result = await repository.getLatLngFromAddress(tAddress); + + expect(result, isA>()); + expect((result as SuccessApiResult).data, tLatLng); + }, + ); + + test( + 'should return ErrorApiResult when remote data source fails', + () async { + when(mockRemoteDataSource.getLatLngFromAddress(tAddress)).thenAnswer( + (_) async => ErrorApiResult(error: "Network Error"), + ); + + final result = await repository.getLatLngFromAddress(tAddress); + + expect(result, isA>()); + expect((result as ErrorApiResult).error, "Network Error"); + }, + ); + }); + + group('getRealRoute', () { + final tMyLocation = LatLng(30.0, 31.0); + final tDestination = LatLng(30.5, 31.5); + final tRoute = [LatLng(30.0, 31.0), LatLng(30.5, 31.5)]; + + test( + 'should return SuccessApiResult when remote data source succeeds', + () async { + when( + mockRemoteDataSource.getRealRoute(tMyLocation, tDestination), + ).thenAnswer((_) async => SuccessApiResult>(data: tRoute)); + + final result = await repository.getRealRoute(tMyLocation, tDestination); + + expect(result, isA>>()); + expect((result as SuccessApiResult).data, tRoute); + }, + ); + + test( + 'should return ErrorApiResult when remote data source fails', + () async { + when( + mockRemoteDataSource.getRealRoute(tMyLocation, tDestination), + ).thenAnswer( + (_) async => ErrorApiResult>(error: "Routing Error"), + ); + + final result = await repository.getRealRoute(tMyLocation, tDestination); + + expect(result, isA>>()); + expect((result as ErrorApiResult).error, "Routing Error"); + }, + ); + }); } diff --git a/test/features/driver_orders_details/domain/models/drivers_model_test.dart b/test/features/driver_orders_details/domain/models/drivers_model_test.dart new file mode 100644 index 0000000..4d2baaf --- /dev/null +++ b/test/features/driver_orders_details/domain/models/drivers_model_test.dart @@ -0,0 +1,30 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; + +void main() { + group('DriverDataModel Tests', () { + test('should correctly initialize DriverDataModel with given values', () { + final dataModel = DriverDataModel( + name: 'mariam', + id: '1', + phone: '01205708282', + deviceToken: 'token', + currentLocation: DriverLocationModel(lat: 30, lng: 29), + ); + + expect(dataModel.name, 'mariam'); + expect(dataModel.currentLocation.lat, 30); + expect(dataModel.id, '1'); + }); + + test( + 'should correctly initialize DriverLocationModel with given values', + () { + final location = DriverLocationModel(lat: 30, lng: 29); + + expect(location.lat, 30); + expect(location.lng, 29); + }, + ); + }); +} diff --git a/test/features/driver_orders_details/domain/usecases/get_driver_data_usecase_test.dart b/test/features/driver_orders_details/domain/usecases/get_driver_data_usecase_test.dart new file mode 100644 index 0000000..aeb2464 --- /dev/null +++ b/test/features/driver_orders_details/domain/usecases/get_driver_data_usecase_test.dart @@ -0,0 +1,64 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/repos/order_details_repo.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/usecases/get_driver_data_usecase.dart'; +import 'get_order_details_usecase_test.mocks.dart'; + +@GenerateMocks([OrderDetailsRepo]) +void main() { + late GetDriverDataUsecase usecase; + late MockOrderDetailsRepo mockRepo; + + setUp(() { + mockRepo = MockOrderDetailsRepo(); + usecase = GetDriverDataUsecase(repo: mockRepo); + provideDummy>>( + ErrorApiResult(error: 'dummy'), + ); + }); + + const driverId = 'pxkMaEmWYVuvV5jkW0JK'; + + final driverModel = DriverDataModel( + id: 'id', + name: 'name', + phone: 'phone', + deviceToken: 'deviceToken', + currentLocation: DriverLocationModel(lat: 30, lng: 29), + ); + + group('GetDriverDataUsecase test', () { + test( + 'should return SuccessApiResult containing the Stream from the repository', + () async { + when( + mockRepo.getDriverData(driverId), + ).thenAnswer((_) => SuccessApiResult(data: Stream.value(driverModel))); + + final result = usecase.call(driverId); + + expect(result, isA>>()); + final stream = + (result as SuccessApiResult>).data; + await expectLater(stream, emits(driverModel)); + verify(mockRepo.getDriverData(driverId)).called(1); + }, + ); + + test('should return ErrorApiResult when the repository fails', () async { + when(mockRepo.getDriverData(driverId)).thenAnswer( + (_) => ErrorApiResult>( + error: 'Error from Repository', + ), + ); + + final result = await usecase.call(driverId); + + expect(result, isA>>()); + expect((result as ErrorApiResult).error, 'Error from Repository'); + }); + }); +} diff --git a/test/features/driver_orders_details/domain/usecases/get_order_details_usecase_test.dart b/test/features/driver_orders_details/domain/usecases/get_order_details_usecase_test.dart index d27570b..c5cba7e 100644 --- a/test/features/driver_orders_details/domain/usecases/get_order_details_usecase_test.dart +++ b/test/features/driver_orders_details/domain/usecases/get_order_details_usecase_test.dart @@ -40,25 +40,25 @@ void main() { test( 'should return SuccessApiResult containing the Stream from the repository', () async { - when( - mockRepo.getOrderDetails(any), - ).thenReturn(SuccessApiResult(data: Stream.value(tOrderModel))); + when(mockRepo.getOrderDetails()).thenAnswer( + (_) async => SuccessApiResult(data: Stream.value(tOrderModel)), + ); - final result = usecase.call(tOrderId); + final result = await usecase.call(); expect(result, isA>>()); final stream = (result as SuccessApiResult>).data; await expectLater(stream, emits(tOrderModel)); - verify(mockRepo.getOrderDetails(tOrderId)).called(1); + verify(mockRepo.getOrderDetails()).called(1); }, ); test('should return ErrorApiResult when the repository fails', () async { when( - mockRepo.getOrderDetails(any), - ).thenReturn(ErrorApiResult(error: 'Error from Repository')); + mockRepo.getOrderDetails(), + ).thenAnswer((_) async => ErrorApiResult(error: 'Error from Repository')); - final result = usecase.call(tOrderId); + final result = await usecase.call(); expect(result, isA>>()); expect((result as ErrorApiResult).error, 'Error from Repository'); diff --git a/test/features/driver_orders_details/presentation/pages/location_page_test.dart b/test/features/driver_orders_details/presentation/pages/location_page_test.dart new file mode 100644 index 0000000..a373b24 --- /dev/null +++ b/test/features/driver_orders_details/presentation/pages/location_page_test.dart @@ -0,0 +1,192 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/drivers_model.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/location_type.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_cubit.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_states.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/pages/location_page.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/widgets/address_card.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/widgets/section_title.dart'; + +import 'drivers_orders_details_page_test.mocks.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + late MockOrderDetailsCubit mockCubit; + final driverData = DriverDataModel( + deviceToken: '', + currentLocation: DriverLocationModel(lat: 30.0, lng: 31.0), + id: '', + name: '', + phone: '', + ); + setUp(() async { + await getIt.reset(); + mockCubit = MockOrderDetailsCubit(); + getIt.registerFactory(() => mockCubit); + when(mockCubit.state).thenReturn(OrderDetailsStates()); + when(mockCubit.stream).thenAnswer((_) => const Stream.empty()); + }); + + Widget buildTestableWidget() { + return EasyLocalization( + supportedLocales: const [Locale('en')], + path: 'assets/translations', + fallbackLocale: const Locale('en'), + startLocale: const Locale('en'), + saveLocale: false, + child: Builder( + builder: (context) { + return MaterialApp( + home: BlocProvider.value( + value: mockCubit, + child: const LocationPage(locationType: LocationType.pickup), + ), + ); + }, + ), + ); + } + + group('Location Page widget test', () { + testWidgets('LocationPage shows loading indicator when driver is null', ( + WidgetTester tester, + ) async { + when( + mockCubit.state, + ).thenReturn(OrderDetailsStates(driverData: Resource.loading())); + when(mockCubit.stream).thenAnswer( + (_) => Stream.value(OrderDetailsStates(driverData: Resource.loading())), + ); + + await tester.pumpWidget(buildTestableWidget()); + await tester.pump(); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + }); + testWidgets('Full LocationPage interaction and listener coverage', ( + tester, + ) async { + final orderData = OrderModel( + userAddress: UserAddressModel(name: '', address: '', userId: ''), + orderId: '', + driverId: '', + userId: '', + orderDetails: OrderDetailsModel( + items: [], + status: '', + totalPrice: 500, + pickupAddress: PickedAddressModel(name: '', address: ''), + orderId: '', + userAddress: '', + ), + ); + + final fullState = OrderDetailsStates( + driverData: Resource.success(driverData), + data: Resource.success(orderData), + polylines: [LatLng(30.0, 31.0), LatLng(30.1, 31.1)], + destination: LatLng(30.1, 31.1), + ); + + when(mockCubit.state).thenReturn(fullState); + when(mockCubit.stream).thenAnswer((_) => Stream.value(fullState)); + when( + mockCubit.setDestinationFromAddress(any, any), + ).thenAnswer((_) async {}); + await tester.pumpWidget(buildTestableWidget()); + await tester.pumpAndSettle(); + + final map = tester.widget(find.byType(GoogleMap)); + expect(map.mapType, MapType.normal); + expect(map.initialCameraPosition.zoom, 18); + + expect(fullState.polylines, isNotEmpty); + expect(fullState.destination, isNotNull); + + verify(mockCubit.setDestinationFromAddress(any, any)).called(1); + }); + + testWidgets('LocationPage shows GoogleMap when driver exists', ( + WidgetTester tester, + ) async { + when(mockCubit.state).thenReturn( + OrderDetailsStates(driverData: Resource.success(driverData)), + ); + when(mockCubit.stream).thenAnswer( + (_) => Stream.value( + OrderDetailsStates( + driverData: Resource.success(driverData), + data: Resource.success(null), + ), + ), + ); + + await tester.pumpWidget(buildTestableWidget()); + await tester.pump(); + + expect(find.byType(Padding), findsWidgets); + expect(find.byType(Scaffold), findsOneWidget); + expect(find.byType(Column), findsWidgets); + expect(find.byType(SizedBox), findsWidgets); + expect(find.byType(GoogleMap), findsOneWidget); + expect( + find.descendant( + of: find.byType(Expanded), + matching: find.byType(GoogleMap), + ), + findsOneWidget, + ); + expect( + find.descendant( + of: find.byType(Stack), + matching: find.byType(GoogleMap), + ), + findsOneWidget, + ); + expect(find.byType(Positioned), findsWidgets); + expect(find.byType(InkWell), findsAtLeast(1)); + expect(find.byType(CircleAvatar), findsNWidgets(3)); + expect(find.byType(AddressCard), findsWidgets); + expect( + find.descendant( + of: find.byType(Column), + matching: find.byType(AddressCard), + ), + findsWidgets, + ); + expect(find.byType(SectionTitle), findsWidgets); + expect( + find.descendant( + of: find.byType(Column), + matching: find.byType(SectionTitle), + ), + findsWidgets, + ); + }); + + testWidgets('Back button is displayed', (WidgetTester tester) async { + when(mockCubit.state).thenReturn( + OrderDetailsStates(driverData: Resource.success(driverData)), + ); + when(mockCubit.stream).thenAnswer( + (_) => Stream.value( + OrderDetailsStates(driverData: Resource.success(driverData)), + ), + ); + + await tester.pumpWidget(buildTestableWidget()); + await tester.pump(); + + expect(find.byIcon(Icons.arrow_back_ios_new), findsOneWidget); + await tester.pump(); + }); + }); +}