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