From 096acae74ee24b5ae3708c702f6cac4aeeb1022a Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Tue, 4 Aug 2026 23:52:27 +0300 Subject: [PATCH] =?UTF-8?q?feat(apps):=20=D8=A7=D9=84=D8=AF=D8=B1=D8=A7?= =?UTF-8?q?=D9=88=D8=B1=20=D9=88=D8=B7=D8=A8=D9=82=D8=AA=D8=A7=20=D8=A7?= =?UTF-8?q?=D9=84=D9=85=D9=8A=D8=B2=D8=A7=D8=AA=20=D9=88=D8=A7=D9=84=D8=A5?= =?UTF-8?q?=D8=B4=D8=B9=D8=A7=D8=B1=D8=A7=D8=AA=20=E2=80=94=20=D8=A7=D9=84?= =?UTF-8?q?=D9=85=D8=B1=D8=AD=D9=84=D8=A9=206?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## طبقتا الميزات مُطبَّقتان معاً AppDrawer يفحص: Features.wallet && context.tenantHas('wallet') - Features.x ثابت const: يقرّر ما يُبنى في الـbinary. إطفاؤه يحذف الكود فلا يُستخرج (docs/23 §5) - tenantHas من GET /tenant/config/:slug: يقرّر ما يُعرض ممّا بُني حسب اشتراك المستأجر (docs/38 §10) هذا هو ربط lite/pro/max عملياً. TenantCubit يعرض النسخة المخزّنة فوراً ثم يحدّثها: إقلاع بلا إنترنت يعرض الميزات المعروفة آخر مرة لا شاشة فارغة. ## الشاشات الدراور · تعديل الحساب · سجلّ الرحلات · الإعدادات (مظهر + لغة) · المساعدة. الرقم في تعديل الحساب معطّل مع سبب معروض لا مخفي: هو هوية الحساب على الخادم وتغييره يعني حساباً آخر (docs/38 §1). ## الإشعارات PushService معزول — الملف الوحيد الذي يعرف Firebase. التسجيل بعد إثبات الجلسة عبر خطّاف في SessionCubit، ومع اشتراك onTokenRefresh: بدونه تتوقّف الإشعارات بصمت بعد أسابيع. الإعداد الأصلي كان جاهزاً مسبقاً. ## تصحيحات بنيوية كشفها فحص آلي docs/23 §1 يمنع أن تستورد ميزة من أخرى — وكانت خمس مخالفات: 1. features/history أُدمجت في features/trip — السجلّ جزء من ميزة الرحلة 2. AppDrawer صعد إلى core/ui — هيكل تطبيق لا ميزة 3. AuthFailure → core/api/api_failure.dart، ومترجمه → core/ui/failure_text.dart 4. AuthHeader → core/ui/form_header.dart 5. ProfileRepository مستقلّ عن AuthRepository: تعديل الملف ليس مصادقة flutter analyze نظيف · الراكب 98 ملف/7,497 سطر · السائق 95 ملف/6,931 سطر. فحوص آلية نظيفة: صفر استيراد بين ميزتين · core يستورد features من di و router وحدهما · Firebase و intaleq_maps و geolocator و socket_io كلٌّ معزول. Co-Authored-By: Claude Opus 5 --- apps/driver_new/lib/app.dart | 18 +- apps/driver_new/lib/core/api/antlaq_api.dart | 1 + apps/driver_new/lib/core/api/api_client.dart | 3 + .../api/api_failure.dart} | 2 +- .../lib/core/api/auth_interceptor.dart | 2 + apps/driver_new/lib/core/design/theme.dart | 3 + apps/driver_new/lib/core/di.dart | 50 ++++- apps/driver_new/lib/core/l10n/app_ar.arb | 29 ++- apps/driver_new/lib/core/l10n/app_en.arb | 29 ++- .../lib/core/notifications/push_service.dart | 66 +++++++ .../lib/core/realtime/realtime_service.dart | 1 + apps/driver_new/lib/core/router.dart | 50 ++++- .../lib/core/session/session_cubit.dart | 18 +- .../lib/core/tenant/feature_gate.dart | 14 ++ .../lib/core/tenant/tenant_config.dart | 79 ++++++++ .../lib/core/tenant/tenant_cubit.dart | 39 ++++ apps/driver_new/lib/core/ui/app_drawer.dart | 186 ++++++++++++++++++ apps/driver_new/lib/core/ui/empty_view.dart | 3 + apps/driver_new/lib/core/ui/error_view.dart | 3 + .../lib/core/ui/failure_text.dart} | 14 +- .../ui/form_header.dart} | 9 +- apps/driver_new/lib/core/ui/skeleton.dart | 1 + .../driver_new/lib/core/ui/status_banner.dart | 1 + .../lib/core/ui/tripz_otp_field.dart | 2 + .../lib/core/ui/tripz_scaffold.dart | 4 + .../lib/core/ui/tripz_text_field.dart | 2 + .../lib/features/auth/cubit/login_cubit.dart | 9 +- .../lib/features/auth/cubit/login_state.dart | 4 +- .../features/auth/data/auth_repository.dart | 5 +- .../features/auth/view/agreement_view.dart | 12 +- .../lib/features/auth/view/login_page.dart | 8 + .../lib/features/auth/view/otp_view.dart | 16 +- .../features/auth/view/permission_view.dart | 12 +- .../lib/features/auth/view/phone_view.dart | 15 +- .../cubit/profile_cubit.dart | 14 +- .../profile/data/profile_repository.dart | 20 ++ .../profile/view/profile_edit_page.dart | 102 ++++++++++ .../profile/view/profile_setup_page.dart} | 38 ++-- .../features/rating/cubit/rating_cubit.dart | 2 + .../lib/features/rating/view/rating_page.dart | 11 ++ .../lib/features/rating/view/star_row.dart | 1 + .../settings/cubit/settings_cubit.dart | 2 + .../settings/cubit/settings_state.dart | 1 + .../features/settings/view/settings_page.dart | 84 ++++++++ .../lib/features/splash/view/splash_page.dart | 13 +- .../features/support/view/support_page.dart | 62 ++++++ .../features/trip/cubit/history_cubit.dart | 44 +++++ .../trip/data/history_repository.dart | 19 ++ .../features/trip/data/maps_repository.dart | 3 + .../features/trip/data/trip_repository.dart | 4 + .../lib/features/trip/view/duty_page.dart | 20 ++ .../lib/features/trip/view/history_page.dart | 112 +++++++++++ .../trip/view/widgets/driver_badge.dart | 3 + .../trip/view/widgets/fare_summary.dart | 5 + .../features/trip/view/widgets/ride_map.dart | 4 + .../features/wallet/cubit/wallet_cubit.dart | 3 + .../features/wallet/cubit/wallet_state.dart | 2 + .../wallet/data/wallet_repository.dart | 2 + .../wallet/view/widgets/balance_card.dart | 3 + .../wallet/view/widgets/txn_tile.dart | 4 + apps/driver_new/lib/main.dart | 3 + apps/driver_new/pubspec.lock | 56 ++++++ apps/driver_new/pubspec.yaml | 4 + apps/rider_new/lib/app.dart | 18 +- apps/rider_new/lib/core/api/antlaq_api.dart | 1 + apps/rider_new/lib/core/api/api_client.dart | 3 + .../api/api_failure.dart} | 2 +- .../lib/core/api/auth_interceptor.dart | 2 + apps/rider_new/lib/core/design/theme.dart | 3 + apps/rider_new/lib/core/di.dart | 50 ++++- apps/rider_new/lib/core/l10n/app_ar.arb | 29 ++- apps/rider_new/lib/core/l10n/app_en.arb | 29 ++- .../lib/core/notifications/push_service.dart | 66 +++++++ .../lib/core/realtime/realtime_service.dart | 1 + apps/rider_new/lib/core/router.dart | 50 ++++- .../lib/core/session/session_cubit.dart | 18 +- .../lib/core/tenant/feature_gate.dart | 14 ++ .../lib/core/tenant/tenant_config.dart | 79 ++++++++ .../lib/core/tenant/tenant_cubit.dart | 39 ++++ apps/rider_new/lib/core/ui/app_drawer.dart | 186 ++++++++++++++++++ apps/rider_new/lib/core/ui/empty_view.dart | 3 + apps/rider_new/lib/core/ui/error_view.dart | 3 + .../lib/core/ui/failure_text.dart} | 14 +- .../ui/form_header.dart} | 9 +- apps/rider_new/lib/core/ui/skeleton.dart | 1 + apps/rider_new/lib/core/ui/status_banner.dart | 1 + .../lib/core/ui/tripz_otp_field.dart | 2 + .../rider_new/lib/core/ui/tripz_scaffold.dart | 4 + .../lib/core/ui/tripz_text_field.dart | 2 + .../lib/features/auth/cubit/login_cubit.dart | 9 +- .../lib/features/auth/cubit/login_state.dart | 4 +- .../features/auth/data/auth_repository.dart | 5 +- .../features/auth/view/agreement_view.dart | 12 +- .../lib/features/auth/view/login_page.dart | 8 + .../lib/features/auth/view/otp_view.dart | 16 +- .../features/auth/view/permission_view.dart | 12 +- .../lib/features/auth/view/phone_view.dart | 15 +- .../cubit/profile_cubit.dart | 14 +- .../profile/data/profile_repository.dart | 20 ++ .../profile/view/profile_edit_page.dart | 102 ++++++++++ .../profile/view/profile_setup_page.dart} | 38 ++-- .../features/rating/cubit/rating_cubit.dart | 2 + .../lib/features/rating/view/rating_page.dart | 11 ++ .../lib/features/rating/view/star_row.dart | 1 + .../settings/cubit/settings_cubit.dart | 2 + .../settings/cubit/settings_state.dart | 1 + .../features/settings/view/settings_page.dart | 84 ++++++++ .../lib/features/splash/view/splash_page.dart | 13 +- .../features/support/view/support_page.dart | 62 ++++++ .../features/trip/cubit/history_cubit.dart | 44 +++++ .../lib/features/trip/cubit/ride_cubit.dart | 10 + .../lib/features/trip/cubit/ride_state.dart | 5 + .../trip/data/history_repository.dart | 19 ++ .../features/trip/data/maps_repository.dart | 3 + .../features/trip/data/trip_repository.dart | 4 + .../lib/features/trip/view/history_page.dart | 112 +++++++++++ .../lib/features/trip/view/ride_page.dart | 57 ++++++ .../trip/view/widgets/active_trip_sheet.dart | 9 + .../trip/view/widgets/cancel_sheet.dart | 3 + .../trip/view/widgets/center_pin.dart | 1 + .../trip/view/widgets/confirm_sheet.dart | 10 + .../trip/view/widgets/driver_badge.dart | 3 + .../trip/view/widgets/fare_summary.dart | 5 + .../trip/view/widgets/planner_sheet.dart | 7 + .../features/trip/view/widgets/ride_map.dart | 4 + .../trip/view/widgets/ride_type_picker.dart | 2 + .../trip/view/widgets/route_timeline.dart | 2 + .../trip/view/widgets/searching_sheet.dart | 5 + .../features/wallet/cubit/wallet_cubit.dart | 3 + .../features/wallet/cubit/wallet_state.dart | 2 + .../wallet/data/wallet_repository.dart | 2 + .../lib/features/wallet/view/wallet_page.dart | 13 ++ .../wallet/view/widgets/balance_card.dart | 3 + .../wallet/view/widgets/txn_tile.dart | 4 + apps/rider_new/lib/main.dart | 3 + apps/rider_new/pubspec.lock | 56 ++++++ apps/rider_new/pubspec.yaml | 4 + docs/37-newapps-cubit-plan.md | 35 +++- 138 files changed, 2716 insertions(+), 125 deletions(-) rename apps/driver_new/lib/{features/auth/cubit/auth_failure.dart => core/api/api_failure.dart} (96%) create mode 100644 apps/driver_new/lib/core/notifications/push_service.dart create mode 100644 apps/driver_new/lib/core/tenant/feature_gate.dart create mode 100644 apps/driver_new/lib/core/tenant/tenant_config.dart create mode 100644 apps/driver_new/lib/core/tenant/tenant_cubit.dart create mode 100644 apps/driver_new/lib/core/ui/app_drawer.dart rename apps/{rider_new/lib/features/auth/view/widgets/auth_failure_text.dart => driver_new/lib/core/ui/failure_text.dart} (89%) rename apps/driver_new/lib/{features/auth/view/widgets/auth_header.dart => core/ui/form_header.dart} (88%) rename apps/driver_new/lib/features/{auth => profile}/cubit/profile_cubit.dart (88%) create mode 100644 apps/driver_new/lib/features/profile/data/profile_repository.dart create mode 100644 apps/driver_new/lib/features/profile/view/profile_edit_page.dart rename apps/{rider_new/lib/features/auth/view/profile_page.dart => driver_new/lib/features/profile/view/profile_setup_page.dart} (87%) create mode 100644 apps/driver_new/lib/features/settings/view/settings_page.dart create mode 100644 apps/driver_new/lib/features/support/view/support_page.dart create mode 100644 apps/driver_new/lib/features/trip/cubit/history_cubit.dart create mode 100644 apps/driver_new/lib/features/trip/data/history_repository.dart create mode 100644 apps/driver_new/lib/features/trip/view/history_page.dart rename apps/rider_new/lib/{features/auth/cubit/auth_failure.dart => core/api/api_failure.dart} (96%) create mode 100644 apps/rider_new/lib/core/notifications/push_service.dart create mode 100644 apps/rider_new/lib/core/tenant/feature_gate.dart create mode 100644 apps/rider_new/lib/core/tenant/tenant_config.dart create mode 100644 apps/rider_new/lib/core/tenant/tenant_cubit.dart create mode 100644 apps/rider_new/lib/core/ui/app_drawer.dart rename apps/{driver_new/lib/features/auth/view/widgets/auth_failure_text.dart => rider_new/lib/core/ui/failure_text.dart} (89%) rename apps/rider_new/lib/{features/auth/view/widgets/auth_header.dart => core/ui/form_header.dart} (88%) rename apps/rider_new/lib/features/{auth => profile}/cubit/profile_cubit.dart (88%) create mode 100644 apps/rider_new/lib/features/profile/data/profile_repository.dart create mode 100644 apps/rider_new/lib/features/profile/view/profile_edit_page.dart rename apps/{driver_new/lib/features/auth/view/profile_page.dart => rider_new/lib/features/profile/view/profile_setup_page.dart} (87%) create mode 100644 apps/rider_new/lib/features/settings/view/settings_page.dart create mode 100644 apps/rider_new/lib/features/support/view/support_page.dart create mode 100644 apps/rider_new/lib/features/trip/cubit/history_cubit.dart create mode 100644 apps/rider_new/lib/features/trip/data/history_repository.dart create mode 100644 apps/rider_new/lib/features/trip/view/history_page.dart diff --git a/apps/driver_new/lib/app.dart b/apps/driver_new/lib/app.dart index 822088d..950cc6d 100644 --- a/apps/driver_new/lib/app.dart +++ b/apps/driver_new/lib/app.dart @@ -1,13 +1,23 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; + import 'package:flutter_localizations/flutter_localizations.dart'; import 'core/build_config.dart'; + import 'core/design/theme.dart'; + import 'core/di.dart'; + import 'core/l10n/l10n.dart'; + import 'core/router.dart'; + +import 'core/tenant/tenant_cubit.dart'; + import 'features/settings/cubit/settings_cubit.dart'; + import 'features/settings/cubit/settings_state.dart'; class TripzApp extends StatelessWidget { @@ -15,8 +25,12 @@ class TripzApp extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => sl(), + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: sl()), + // إعداد المستأجر فوق الشجرة: كل شاشة تسأله عن ميزاتها بلا تمرير. + BlocProvider.value(value: sl()), + ], child: BlocBuilder( builder: (context, settings) { return MaterialApp.router( diff --git a/apps/driver_new/lib/core/api/antlaq_api.dart b/apps/driver_new/lib/core/api/antlaq_api.dart index 9470484..3b4990c 100644 --- a/apps/driver_new/lib/core/api/antlaq_api.dart +++ b/apps/driver_new/lib/core/api/antlaq_api.dart @@ -1,6 +1,7 @@ import 'package:dio/dio.dart'; import '../config.dart'; + import 'api_exception.dart'; /// عميل map-saas — **منفصل تماماً** عن `ApiClient`. diff --git a/apps/driver_new/lib/core/api/api_client.dart b/apps/driver_new/lib/core/api/api_client.dart index 6f03250..04943f9 100644 --- a/apps/driver_new/lib/core/api/api_client.dart +++ b/apps/driver_new/lib/core/api/api_client.dart @@ -1,8 +1,11 @@ import 'package:dio/dio.dart'; import '../config.dart'; + import '../storage/token_store.dart'; + import 'api_exception.dart'; + import 'auth_interceptor.dart'; /// الواجهة الوحيدة للخادم. لا تُنشأ `Dio` في أي مكان آخر. diff --git a/apps/driver_new/lib/features/auth/cubit/auth_failure.dart b/apps/driver_new/lib/core/api/api_failure.dart similarity index 96% rename from apps/driver_new/lib/features/auth/cubit/auth_failure.dart rename to apps/driver_new/lib/core/api/api_failure.dart index 2efe08f..ba6d221 100644 --- a/apps/driver_new/lib/features/auth/cubit/auth_failure.dart +++ b/apps/driver_new/lib/core/api/api_failure.dart @@ -1,4 +1,4 @@ -import '../../../core/api/api_exception.dart'; +import 'api_exception.dart'; /// سبب الفشل كـ**رمز** لا كنصّ. /// diff --git a/apps/driver_new/lib/core/api/auth_interceptor.dart b/apps/driver_new/lib/core/api/auth_interceptor.dart index 4663e27..ebda130 100644 --- a/apps/driver_new/lib/core/api/auth_interceptor.dart +++ b/apps/driver_new/lib/core/api/auth_interceptor.dart @@ -3,7 +3,9 @@ import 'dart:async'; import 'package:dio/dio.dart'; import '../build_config.dart'; + import '../config.dart'; + import '../storage/token_store.dart'; /// يثبّت ترويسات كل طلب، ويجدّد التوكن **استباقياً** قبل انتهائه. diff --git a/apps/driver_new/lib/core/design/theme.dart b/apps/driver_new/lib/core/design/theme.dart index f375f8e..0b7204c 100644 --- a/apps/driver_new/lib/core/design/theme.dart +++ b/apps/driver_new/lib/core/design/theme.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import '../build_config.dart'; + import 'tokens.dart'; + import 'tripz_colors.dart'; + import 'typography.dart'; /// دالة بناء الثيم الوحيدة (docs/26 §4): مدخلان لا ثالث لهما. diff --git a/apps/driver_new/lib/core/di.dart b/apps/driver_new/lib/core/di.dart index 1839812..426585b 100644 --- a/apps/driver_new/lib/core/di.dart +++ b/apps/driver_new/lib/core/di.dart @@ -1,28 +1,57 @@ import 'package:device_info_plus/device_info_plus.dart'; + import 'package:flutter/foundation.dart'; + import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + import 'package:get_it/get_it.dart'; + import 'package:shared_preferences/shared_preferences.dart'; import '../features/auth/cubit/login_cubit.dart'; + import '../features/auth/data/auth_repository.dart'; +import '../features/profile/data/profile_repository.dart'; + import '../features/rating/cubit/rating_cubit.dart'; + import '../features/rating/data/rating_repository.dart'; + import '../features/settings/cubit/settings_cubit.dart'; + +import '../features/trip/cubit/history_cubit.dart'; + import '../features/trip/cubit/duty_cubit.dart'; + +import '../features/trip/data/history_repository.dart'; + +import '../features/trip/data/maps_repository.dart'; + +import '../features/trip/data/trip_repository.dart'; + import '../features/wallet/cubit/payout_cubit.dart'; import '../features/wallet/cubit/wallet_cubit.dart'; + import '../features/wallet/data/wallet_repository.dart'; -import '../features/trip/data/maps_repository.dart'; -import '../features/trip/data/trip_repository.dart'; + import 'api/antlaq_api.dart'; + import 'api/api_client.dart'; -import 'location/location_service.dart'; -import 'realtime/realtime_service.dart'; -import 'session/session_cubit.dart'; + import 'build_config.dart'; + +import 'location/location_service.dart'; + +import 'notifications/push_service.dart'; + +import 'realtime/realtime_service.dart'; + +import 'session/session_cubit.dart'; + import 'storage/token_store.dart'; +import 'tenant/tenant_cubit.dart'; + final sl = GetIt.instance; /// نقطة الإقفال الوحيدة (docs/23 §5): الميزة المطفأة **لا تُسجَّل**، بدل @@ -52,7 +81,10 @@ Future setupInjector() async { ); sl.registerSingleton(AuthRepository(sl(), sl())); - sl.registerSingleton(SessionCubit(sl())); + sl.registerSingleton(ProfileRepository(sl())); + sl.registerSingleton( + SessionCubit(sl(), onAuthenticated: () => sl().register()), + ); // تدفّق الدخول قصير العمر: نسخة جديدة لكل دخول، لا نسخة واحدة أبدية. sl.registerFactory(() => LoginCubit(sl(), sl())); @@ -64,6 +96,12 @@ Future setupInjector() async { sl.registerSingleton(LocationService()); sl.registerSingleton(RealtimeService(tokens)); + sl.registerSingleton(TenantCubit(sl(), prefs)); + sl.registerSingleton(PushService(sl())); + + sl.registerSingleton(HistoryRepository(sl())); + sl.registerFactory(() => HistoryCubit(sl())); + sl.registerSingleton(RatingRepository(sl())); sl.registerFactory(() => RatingCubit(sl())); diff --git a/apps/driver_new/lib/core/l10n/app_ar.arb b/apps/driver_new/lib/core/l10n/app_ar.arb index 9f4f78e..11df7c7 100644 --- a/apps/driver_new/lib/core/l10n/app_ar.arb +++ b/apps/driver_new/lib/core/l10n/app_ar.arb @@ -161,5 +161,32 @@ "payoutDone": "تم إرسال طلب السحب", "payoutFailed": "تعذّر إتمام السحب", "payoutInsufficient": "الرصيد لا يكفي", - "payoutHistory": "طلبات السحب" + "payoutHistory": "طلبات السحب", + "menuTitle": "القائمة", + "menuProfile": "حسابي", + "menuTrips": "رحلاتي", + "menuWallet": "المحفظة", + "menuEarnings": "أرباحي", + "menuSettings": "الإعدادات", + "menuSupport": "المساعدة", + "menuAbout": "عن التطبيق", + "menuLogout": "تسجيل الخروج", + "menuLogoutConfirm": "هل تريد الخروج من حسابك؟", + "historyTitle": "رحلاتي", + "historyEmpty": "لا رحلات بعد — اتصل لتصلك الطلبات", + "historyStatusCompleted": "مكتملة", + "historyStatusCancelled": "ملغاة", + "historyStatusLive": "جارية", + "profileEditTitle": "تعديل الحساب", + "profilePhone": "رقم الهاتف", + "profilePhoneLocked": "لا يمكن تغيير الرقم — هو هوية حسابك", + "profileSaved": "حُفظت التعديلات", + "supportTitle": "المساعدة", + "supportFaq": "الأسئلة الشائعة", + "supportContact": "تواصل معنا", + "supportWhatsapp": "مراسلة عبر واتساب", + "aboutVersion": "الإصدار", + "notifTitle": "الإشعارات", + "notifEnable": "تفعيل الإشعارات", + "notifDenied": "الإشعارات موقوفة من إعدادات النظام" } \ No newline at end of file diff --git a/apps/driver_new/lib/core/l10n/app_en.arb b/apps/driver_new/lib/core/l10n/app_en.arb index cc8c01e..ffdb654 100644 --- a/apps/driver_new/lib/core/l10n/app_en.arb +++ b/apps/driver_new/lib/core/l10n/app_en.arb @@ -161,5 +161,32 @@ "payoutDone": "Withdrawal request sent", "payoutFailed": "Withdrawal failed", "payoutInsufficient": "Insufficient balance", - "payoutHistory": "Withdrawal requests" + "payoutHistory": "Withdrawal requests", + "menuTitle": "Menu", + "menuProfile": "Account", + "menuTrips": "My trips", + "menuWallet": "Wallet", + "menuEarnings": "Earnings", + "menuSettings": "Settings", + "menuSupport": "Help", + "menuAbout": "About", + "menuLogout": "Sign out", + "menuLogoutConfirm": "Sign out of your account?", + "historyTitle": "My trips", + "historyEmpty": "No trips yet — go online to receive requests", + "historyStatusCompleted": "Completed", + "historyStatusCancelled": "Cancelled", + "historyStatusLive": "In progress", + "profileEditTitle": "Edit account", + "profilePhone": "Phone number", + "profilePhoneLocked": "Your number can't be changed — it identifies your account", + "profileSaved": "Changes saved", + "supportTitle": "Help", + "supportFaq": "FAQ", + "supportContact": "Contact us", + "supportWhatsapp": "Message on WhatsApp", + "aboutVersion": "Version", + "notifTitle": "Notifications", + "notifEnable": "Enable notifications", + "notifDenied": "Notifications are disabled in system settings" } \ No newline at end of file diff --git a/apps/driver_new/lib/core/notifications/push_service.dart b/apps/driver_new/lib/core/notifications/push_service.dart new file mode 100644 index 0000000..f9de3a7 --- /dev/null +++ b/apps/driver_new/lib/core/notifications/push_service.dart @@ -0,0 +1,66 @@ +import 'package:firebase_core/firebase_core.dart'; + +import 'package:firebase_messaging/firebase_messaging.dart'; + +import 'package:flutter/foundation.dart'; + +import '../api/api_client.dart'; + +import '../api/api_exception.dart'; + +/// إشعارات FCM — **الطبقة الوحيدة التي تعرف Firebase**. +/// +/// عقد الخادم نقطة واحدة: `POST /notifications/token { token, platform }` +/// (docs/38 §11). لا نقطة سرد إشعارات، فلا شاشة «صندوق إشعارات» تُبنى على +/// وهم — ما يصل يصل من النظام. +/// +/// الإعداد الأصلي جاهز مسبقاً: `google-services.json` و +/// `GoogleService-Info.plist` منقولان مع الطبقة الأصلية، و`build.gradle` +/// يطبّق `com.google.gms.google-services` أصلاً. +class PushService { + PushService(this._api); + + final ApiClient _api; + + /// يُنادى **بعد** الدخول: التسجيل يربط الجهاز بالمستخدم، فقبل الجلسة + /// لا معنى له. + Future register() async { + try { + await Firebase.initializeApp(); + final messaging = FirebaseMessaging.instance; + + final settings = await messaging.requestPermission(); + if (settings.authorizationStatus == AuthorizationStatus.denied) return; + + final token = await messaging.getToken(); + if (token != null) await _send(token); + + // الرمز يُدوَّر من Google دورياً — بلا هذا الاشتراك تتوقّف الإشعارات + // بصمت بعد أسابيع. + messaging.onTokenRefresh.listen(_send); + } catch (_) { + // غياب الإشعارات لا يمنع استعمال التطبيق — لا يُسقط الإقلاع. + } + } + + Future _send(String token) async { + try { + await _api.post>( + '/notifications/token', + body: { + 'token': token, + 'platform': defaultTargetPlatformName, + }, + ); + } on ApiException { + // يُعاد في الإقلاع التالي. + } + } +} + +/// المنصّة كما يتوقّعها الخادم — تُقرأ من المنصّة الفعلية لا من عَلَم بناء. +String get defaultTargetPlatformName => switch (defaultTargetPlatform) { + TargetPlatform.iOS => 'ios', + TargetPlatform.android => 'android', + _ => 'unknown', + }; diff --git a/apps/driver_new/lib/core/realtime/realtime_service.dart b/apps/driver_new/lib/core/realtime/realtime_service.dart index 48c8817..29b0022 100644 --- a/apps/driver_new/lib/core/realtime/realtime_service.dart +++ b/apps/driver_new/lib/core/realtime/realtime_service.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:socket_io_client/socket_io_client.dart' as io; import '../config.dart'; + import '../storage/token_store.dart'; /// أحداث الخادم كما هي في `backend-archive/src/realtime` (docs/38 §9). diff --git a/apps/driver_new/lib/core/router.dart b/apps/driver_new/lib/core/router.dart index c727bc8..1135f09 100644 --- a/apps/driver_new/lib/core/router.dart +++ b/apps/driver_new/lib/core/router.dart @@ -1,19 +1,37 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; + import 'package:go_router/go_router.dart'; -import 'session/session_cubit.dart'; -import 'session/session_state.dart'; import '../features/auth/view/login_page.dart'; -import '../features/auth/view/profile_page.dart'; + +import '../features/profile/view/profile_edit_page.dart'; + +import '../features/profile/view/profile_setup_page.dart'; + import '../features/rating/data/rating_repository.dart'; + import '../features/rating/view/rating_page.dart'; -import '../features/trip/view/duty_page.dart'; -import '../features/wallet/view/earnings_page.dart'; + +import '../features/settings/view/settings_page.dart'; + import '../features/splash/view/splash_page.dart'; + +import '../features/support/view/support_page.dart'; + +import '../features/trip/view/history_page.dart'; + +import '../features/trip/view/duty_page.dart'; + +import '../features/wallet/view/earnings_page.dart'; + import 'di.dart'; +import 'session/session_cubit.dart'; + +import 'session/session_state.dart'; + class Routes { const Routes._(); static const splash = '/'; @@ -22,6 +40,10 @@ class Routes { static const home = '/home'; static const rating = '/rating'; static const wallet = '/wallet'; + static const history = '/history'; + static const profileEdit = '/account'; + static const settings = '/settings'; + static const support = '/support'; } /// التوجيه يُشتقّ من `SessionCubit` وحده — لا `Navigator.push` بعد الدخول @@ -64,7 +86,7 @@ final appRouter = GoRouter( ), GoRoute( path: Routes.profile, - builder: (context, state) => const ProfilePage(), + builder: (context, state) => const ProfileSetupPage(), ), GoRoute( path: Routes.rating, @@ -83,6 +105,22 @@ final appRouter = GoRouter( path: Routes.wallet, builder: (context, state) => const EarningsPage(), ), + GoRoute( + path: Routes.history, + builder: (context, state) => const HistoryPage(), + ), + GoRoute( + path: Routes.profileEdit, + builder: (context, state) => const ProfileEditPage(), + ), + GoRoute( + path: Routes.settings, + builder: (context, state) => const SettingsPage(), + ), + GoRoute( + path: Routes.support, + builder: (context, state) => const SupportPage(), + ), GoRoute( path: Routes.home, builder: (context, state) => const DutyPage(), diff --git a/apps/driver_new/lib/core/session/session_cubit.dart b/apps/driver_new/lib/core/session/session_cubit.dart index 3a135c2..622377a 100644 --- a/apps/driver_new/lib/core/session/session_cubit.dart +++ b/apps/driver_new/lib/core/session/session_cubit.dart @@ -5,16 +5,24 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../api/api_exception.dart'; import 'app_user.dart'; + import 'session_source.dart'; + import 'session_state.dart'; /// حالة الجلسة على مستوى التطبيق — مصدر قرار التوجيه الوحيد /// (`core/router.dart` يستمع إليها). class SessionCubit extends Cubit { - SessionCubit(this._repo) : super(const SessionState()); + SessionCubit(this._repo, {Future Function()? onAuthenticated}) + : _onAuthenticated = onAuthenticated, + super(const SessionState()); final SessionSource _repo; + /// خطّاف يُنفَّذ مرة عند إثبات الجلسة — يبقي `core/session` جاهلاً بمن + /// يستمع إليه. + final Future Function()? _onAuthenticated; + /// تُستدعى مرة عند الإقلاع من شاشة الـsplash. Future restore() async { if (!_repo.hasSession) { @@ -60,8 +68,12 @@ class SessionCubit extends Cubit { user: user, pendingRating: state.pendingRating, )); - // التقييم المعلّق يُفحص بعد إثبات الجلسة لا قبلها. - if (!user.needsProfile) unawaited(_checkPendingRating()); + // بعد إثبات الجلسة: التقييم المعلّق، وتسجيل رمز الإشعارات (يربط الجهاز + // بالمستخدم، فلا معنى له قبل الدخول). + if (!user.needsProfile) { + unawaited(_checkPendingRating()); + unawaited(_onAuthenticated?.call() ?? Future.value()); + } } Future _checkPendingRating() async { diff --git a/apps/driver_new/lib/core/tenant/feature_gate.dart b/apps/driver_new/lib/core/tenant/feature_gate.dart new file mode 100644 index 0000000..9d51ae4 --- /dev/null +++ b/apps/driver_new/lib/core/tenant/feature_gate.dart @@ -0,0 +1,14 @@ +import 'package:flutter/widgets.dart'; + +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'tenant_cubit.dart'; + +extension FeatureGateX on BuildContext { + /// هل الميزة مفعّلة **لهذا المستأجر** وقت التشغيل؟ + /// + /// تُقرأ مع علم `Features` الثابت: الأول يقرّر ما بُني، وهذا ما يُعرض. + /// أيّهما `false` يُخفي الميزة. + bool tenantHas(String feature) => + watch().state.has(feature); +} diff --git a/apps/driver_new/lib/core/tenant/tenant_config.dart b/apps/driver_new/lib/core/tenant/tenant_config.dart new file mode 100644 index 0000000..daad1c4 --- /dev/null +++ b/apps/driver_new/lib/core/tenant/tenant_config.dart @@ -0,0 +1,79 @@ +import 'dart:convert'; + +import 'package:equatable/equatable.dart'; + +/// إعداد المستأجر من `GET /tenant/config/:slug` (شكل مُتحقَّق حيّاً، +/// docs/38 §10) — بلا توكن، فيُجلب قبل الدخول. +/// +/// **طبقتا الميزات**، ولكلٍّ دورها: +/// - `Features` (`const` في `build_config.dart`) يقرّر ما **يُبنى** في +/// الـbinary — إطفاؤه يحذف الكود فلا يُستخرج (docs/23 §5). +/// - هذا الإعداد يقرّر ما **يُعرض** ممّا بُني، حسب ما اشتراه المستأجر فعلاً. +/// الشاشة تتحقّق من الاثنتين: `Features.wallet && tenant.has('wallet')`. +class TenantConfig extends Equatable { + const TenantConfig({ + required this.slug, + required this.name, + required this.countryPack, + required this.plan, + required this.features, + }); + + final String slug; + final String name; + + /// يحدّد مزوّد الرسائل والعملة وصيغة الهاتف — `jo`/`eg`/`sy`. + final String countryPack; + + final String plan; + final Map features; + + static const empty = TenantConfig( + slug: '', + name: '', + countryPack: '', + plan: '', + features: {}, + ); + + /// الافتراض `false` عند غياب المفتاح: ميزة غير معلنة = غير مشتراة. + bool has(String feature) => features[feature] ?? false; + + factory TenantConfig.fromJson(Map json) { + final raw = json['features']; + return TenantConfig( + slug: json['slug'] as String? ?? '', + name: json['name'] as String? ?? '', + countryPack: json['countryPack'] as String? ?? '', + plan: json['plan'] as String? ?? '', + features: raw is Map + ? { + for (final e in raw.entries) + e.key.toString(): e.value == true, + } + : const {}, + ); + } + + Map toJson() => { + 'slug': slug, + 'name': name, + 'countryPack': countryPack, + 'plan': plan, + 'features': features, + }; + + String encode() => json.encode(toJson()); + + static TenantConfig? decode(String? raw) { + if (raw == null || raw.isEmpty) return null; + try { + return TenantConfig.fromJson(json.decode(raw) as Map); + } catch (_) { + return null; + } + } + + @override + List get props => [slug, name, countryPack, plan, features]; +} diff --git a/apps/driver_new/lib/core/tenant/tenant_cubit.dart b/apps/driver_new/lib/core/tenant/tenant_cubit.dart new file mode 100644 index 0000000..1ae1444 --- /dev/null +++ b/apps/driver_new/lib/core/tenant/tenant_cubit.dart @@ -0,0 +1,39 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:shared_preferences/shared_preferences.dart'; + +import '../api/api_client.dart'; + +import '../api/api_exception.dart'; + +import '../build_config.dart'; + +import 'tenant_config.dart'; + +/// يجلب إعداد المستأجر مرة عند الإقلاع، ويخزّنه محلياً. +/// +/// **النسخة المخزّنة تُعرض فوراً** ثم تُحدَّث من الشبكة: إقلاعٌ بلا إنترنت +/// يجب أن يعرض التطبيق بميزاته المعروفة آخر مرة، لا شاشة فارغة. +class TenantCubit extends Cubit { + TenantCubit(this._api, this._prefs) + : super(TenantConfig.decode(_prefs.getString(_key)) ?? TenantConfig.empty); + + final ApiClient _api; + final SharedPreferences _prefs; + + static const _key = 'tenant.config'; + + Future load() async { + try { + final res = await _api.get>( + '/tenant/config/${BuildConfig.tenantSlug}', + ); + final config = TenantConfig.fromJson(res); + emit(config); + await _prefs.setString(_key, config.encode()); + } on ApiException { + // الفشل يُبقي النسخة المخزّنة — لا يُطفئ ميزات المستخدم لأن الشبكة + // تعثّرت لحظة الإقلاع. + } + } +} diff --git a/apps/driver_new/lib/core/ui/app_drawer.dart b/apps/driver_new/lib/core/ui/app_drawer.dart new file mode 100644 index 0000000..a164663 --- /dev/null +++ b/apps/driver_new/lib/core/ui/app_drawer.dart @@ -0,0 +1,186 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:go_router/go_router.dart'; + +import '../build_config.dart'; + +import '../config.dart'; + +import '../design/tokens.dart'; + +import '../design/tripz_colors.dart'; + +import '../l10n/l10n.dart'; + +import '../router.dart'; + +import '../session/session_cubit.dart'; + +import '../session/session_state.dart'; + +import '../tenant/feature_gate.dart'; + +/// الدراور — نقطة تطبيق **طبقتَي الميزات** معاً (docs/23 §5 + docs/38 §10): +/// +/// * `Features.x` ثابت: يقرّر ما بُني في الـbinary. إطفاؤه يحذف الكود. +/// * `context.tenantHas('x')`: يقرّر ما يُعرض ممّا بُني، حسب اشتراك المستأجر. +/// +/// أيّهما `false` يُخفي المدخل — ولا يُكتب `if` ثانٍ في أي شاشة. +class AppDrawer extends StatelessWidget { + const AppDrawer({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final isDriver = AppConfig.appRole == 'driver'; + + return Drawer( + child: SafeArea( + child: Column( + children: [ + const _Header(), + const Divider(height: 1), + Expanded( + child: ListView( + padding: const EdgeInsets.symmetric(vertical: Space.xs), + children: [ + _Item( + icon: Icons.person_outline_rounded, + label: l10n.menuProfile, + route: Routes.profileEdit, + ), + _Item( + icon: Icons.route_outlined, + label: l10n.menuTrips, + route: Routes.history, + ), + if (Features.wallet && context.tenantHas('wallet')) + _Item( + icon: Icons.account_balance_wallet_outlined, + label: isDriver ? l10n.menuEarnings : l10n.menuWallet, + route: Routes.wallet, + ), + _Item( + icon: Icons.settings_outlined, + label: l10n.menuSettings, + route: Routes.settings, + ), + _Item( + icon: Icons.help_outline_rounded, + label: l10n.menuSupport, + route: Routes.support, + ), + ], + ), + ), + const Divider(height: 1), + ListTile( + leading: Icon( + Icons.logout_rounded, + color: context.tripzColors.danger, + ), + title: Text( + l10n.menuLogout, + style: TextStyle(color: context.tripzColors.danger), + ), + onTap: () => _confirmLogout(context), + ), + ], + ), + ), + ); + } + + Future _confirmLogout(BuildContext context) async { + final l10n = context.l10n; + final session = context.read(); + final ok = await showDialog( + context: context, + builder: (context) => AlertDialog( + content: Text(l10n.menuLogoutConfirm), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(l10n.actionCancel), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(l10n.menuLogout), + ), + ], + ), + ); + // الخروج يمرّ بالجلسة وحدها؛ الراوتر يعيد التوجيه من تلقائه. + if (ok == true) await session.logout(); + } +} + +class _Header extends StatelessWidget { + const _Header(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, session) { + final user = session.user; + return Padding( + padding: Space.page, + child: Row( + children: [ + CircleAvatar( + radius: 26, + backgroundColor: context.colors.primaryContainer, + child: Icon( + Icons.person_rounded, + color: context.colors.onPrimaryContainer, + ), + ), + const SizedBox(width: Space.sm), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + user?.name ?? BuildConfig.appName, + style: context.texts.titleMedium, + overflow: TextOverflow.ellipsis, + ), + Text( + user?.phone ?? '', + textDirection: TextDirection.ltr, + style: context.texts.bodySmall?.copyWith( + color: context.colors.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + } +} + +class _Item extends StatelessWidget { + const _Item({required this.icon, required this.label, required this.route}); + + final IconData icon; + final String label; + final String route; + + @override + Widget build(BuildContext context) { + return ListTile( + leading: Icon(icon), + title: Text(label), + onTap: () { + Navigator.of(context).pop(); + context.push(route); + }, + ); + } +} diff --git a/apps/driver_new/lib/core/ui/empty_view.dart b/apps/driver_new/lib/core/ui/empty_view.dart index bae9cdb..bcd69aa 100644 --- a/apps/driver_new/lib/core/ui/empty_view.dart +++ b/apps/driver_new/lib/core/ui/empty_view.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import '../design/tokens.dart'; + import '../design/tripz_colors.dart'; + import '../l10n/l10n.dart'; + import 'tripz_button.dart'; /// حالة الفراغ: رسالة **تدلّ على الفعل التالي** لا مجرّد «لا يوجد» diff --git a/apps/driver_new/lib/core/ui/error_view.dart b/apps/driver_new/lib/core/ui/error_view.dart index 0e0eb7c..e348218 100644 --- a/apps/driver_new/lib/core/ui/error_view.dart +++ b/apps/driver_new/lib/core/ui/error_view.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import '../design/tokens.dart'; + import '../design/tripz_colors.dart'; + import '../l10n/l10n.dart'; + import 'tripz_button.dart'; /// حالة الخطأ: رسالة **بالعربية** مع «أعد المحاولة» — لا شاشة صامتة diff --git a/apps/rider_new/lib/features/auth/view/widgets/auth_failure_text.dart b/apps/driver_new/lib/core/ui/failure_text.dart similarity index 89% rename from apps/rider_new/lib/features/auth/view/widgets/auth_failure_text.dart rename to apps/driver_new/lib/core/ui/failure_text.dart index 6936ea3..2b7cd48 100644 --- a/apps/rider_new/lib/features/auth/view/widgets/auth_failure_text.dart +++ b/apps/driver_new/lib/core/ui/failure_text.dart @@ -1,10 +1,14 @@ import 'package:flutter/material.dart'; -import '../../../../core/design/tokens.dart'; -import '../../../../core/design/tripz_colors.dart'; -import '../../../../core/l10n/l10n.dart'; -import '../../cubit/auth_failure.dart'; -import '../../cubit/phone_error.dart'; +import '../../features/auth/cubit/phone_error.dart'; + +import '../api/api_failure.dart'; + +import '../design/tokens.dart'; + +import '../design/tripz_colors.dart'; + +import '../l10n/l10n.dart'; /// النقطة **الوحيدة** التي تُترجَم فيها رموز الفشل إلى نصّ. /// الشاشات لا تكتب رسائل خطأ، والـCubit لا يعرف اللغة (راجع `auth_failure.dart`). diff --git a/apps/driver_new/lib/features/auth/view/widgets/auth_header.dart b/apps/driver_new/lib/core/ui/form_header.dart similarity index 88% rename from apps/driver_new/lib/features/auth/view/widgets/auth_header.dart rename to apps/driver_new/lib/core/ui/form_header.dart index 1de0d2d..201213d 100644 --- a/apps/driver_new/lib/features/auth/view/widgets/auth_header.dart +++ b/apps/driver_new/lib/core/ui/form_header.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; -import '../../../../core/design/tokens.dart'; -import '../../../../core/design/tripz_colors.dart'; +import '../design/tokens.dart'; + +import '../design/tripz_colors.dart'; /// ترويسة موحّدة لشاشات الدخول: أيقونة ثم عنوان ثم سطر شارح. /// «الشاشة مهمة واحدة» — سؤال واحد وزر أساسي واحد أسفلها (docs/26 §8.1). -class AuthHeader extends StatelessWidget { - const AuthHeader({ +class FormHeader extends StatelessWidget { + const FormHeader({ super.key, required this.icon, required this.title, diff --git a/apps/driver_new/lib/core/ui/skeleton.dart b/apps/driver_new/lib/core/ui/skeleton.dart index 9c5fddb..f72c366 100644 --- a/apps/driver_new/lib/core/ui/skeleton.dart +++ b/apps/driver_new/lib/core/ui/skeleton.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../design/tokens.dart'; + import '../design/tripz_colors.dart'; /// هيكل تحميل — **لا spinner فارغ** (docs/26 §8.4). diff --git a/apps/driver_new/lib/core/ui/status_banner.dart b/apps/driver_new/lib/core/ui/status_banner.dart index b2e3c9d..4007c18 100644 --- a/apps/driver_new/lib/core/ui/status_banner.dart +++ b/apps/driver_new/lib/core/ui/status_banner.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../design/tokens.dart'; + import '../design/tripz_colors.dart'; enum BannerTone { info, success, warning, danger } diff --git a/apps/driver_new/lib/core/ui/tripz_otp_field.dart b/apps/driver_new/lib/core/ui/tripz_otp_field.dart index 0779f88..ad1a22d 100644 --- a/apps/driver_new/lib/core/ui/tripz_otp_field.dart +++ b/apps/driver_new/lib/core/ui/tripz_otp_field.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; + import 'package:flutter/services.dart'; import '../design/tokens.dart'; + import '../design/typography.dart'; /// حقل رمز التحقّق — أربع خانات (طول رمز تريبز، docs/38 §2). diff --git a/apps/driver_new/lib/core/ui/tripz_scaffold.dart b/apps/driver_new/lib/core/ui/tripz_scaffold.dart index 8e5fdcf..9972c2b 100644 --- a/apps/driver_new/lib/core/ui/tripz_scaffold.dart +++ b/apps/driver_new/lib/core/ui/tripz_scaffold.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; import '../design/tokens.dart'; + import 'empty_view.dart'; + import 'error_view.dart'; + import 'skeleton.dart'; + import 'view_status.dart'; /// نقطة التنظيم المركزية (docs/26 §6). **ممنوع `Scaffold` خام في diff --git a/apps/driver_new/lib/core/ui/tripz_text_field.dart b/apps/driver_new/lib/core/ui/tripz_text_field.dart index b0e0acb..a070057 100644 --- a/apps/driver_new/lib/core/ui/tripz_text_field.dart +++ b/apps/driver_new/lib/core/ui/tripz_text_field.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; + import 'package:flutter/services.dart'; import '../design/tokens.dart'; + import '../design/typography.dart'; /// حقل الإدخال الموحّد (docs/26 §7): نصّ · هاتف · بحث. diff --git a/apps/driver_new/lib/features/auth/cubit/login_cubit.dart b/apps/driver_new/lib/features/auth/cubit/login_cubit.dart index 389a2d4..cf11776 100644 --- a/apps/driver_new/lib/features/auth/cubit/login_cubit.dart +++ b/apps/driver_new/lib/features/auth/cubit/login_cubit.dart @@ -1,14 +1,21 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; + import 'package:permission_handler/permission_handler.dart'; + import 'package:shared_preferences/shared_preferences.dart'; import '../../../core/api/api_exception.dart'; + +import '../../../core/api/api_failure.dart'; + import '../../../core/config.dart'; + import '../data/auth_repository.dart'; -import 'auth_failure.dart'; + import 'login_state.dart'; + import 'phone_error.dart'; /// تدفّق الدخول كاملاً: الشروط ← إذن الموقع ← الهاتف ← الرمز. diff --git a/apps/driver_new/lib/features/auth/cubit/login_state.dart b/apps/driver_new/lib/features/auth/cubit/login_state.dart index 9621f23..79f38ef 100644 --- a/apps/driver_new/lib/features/auth/cubit/login_state.dart +++ b/apps/driver_new/lib/features/auth/cubit/login_state.dart @@ -1,7 +1,9 @@ import 'package:equatable/equatable.dart'; +import '../../../core/api/api_failure.dart'; + import '../../../core/session/app_user.dart'; -import 'auth_failure.dart'; + import 'phone_error.dart'; /// خطوات الدخول بالترتيب الميداني المثبت في سيرو (docs/39 §2): بوابتان diff --git a/apps/driver_new/lib/features/auth/data/auth_repository.dart b/apps/driver_new/lib/features/auth/data/auth_repository.dart index e8ac180..3cc1ea3 100644 --- a/apps/driver_new/lib/features/auth/data/auth_repository.dart +++ b/apps/driver_new/lib/features/auth/data/auth_repository.dart @@ -1,8 +1,11 @@ import '../../../core/api/api_client.dart'; -import '../../../core/storage/token_store.dart'; + import '../../../core/session/app_user.dart'; + import '../../../core/session/session_source.dart'; +import '../../../core/storage/token_store.dart'; + /// كل نداءات المصادقة (docs/38 §2). الـCubit لا ينادي Dio مباشرة أبداً /// (docs/23 §3). class AuthRepository implements SessionSource { diff --git a/apps/driver_new/lib/features/auth/view/agreement_view.dart b/apps/driver_new/lib/features/auth/view/agreement_view.dart index 1f31675..8a77f9c 100644 --- a/apps/driver_new/lib/features/auth/view/agreement_view.dart +++ b/apps/driver_new/lib/features/auth/view/agreement_view.dart @@ -1,14 +1,22 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/design/tokens.dart'; + import '../../../core/design/tripz_colors.dart'; + import '../../../core/l10n/l10n.dart'; + import '../../../core/ui/tripz_button.dart'; + import '../../../core/ui/tripz_scaffold.dart'; + import '../cubit/login_cubit.dart'; + import '../cubit/login_state.dart'; -import 'widgets/auth_header.dart'; + +import '../../../core/ui/form_header.dart'; /// البوابة الأولى: شاشة كاملة لا حوار، وزر متابعة **معطّل حتى يُؤشَّر** /// (docs/39 §2). تُحفظ الموافقة فلا تتكرر. @@ -31,7 +39,7 @@ class AgreementView extends StatelessWidget { child: ListView( children: [ const SizedBox(height: Space.xl), - AuthHeader( + FormHeader( icon: Icons.policy_outlined, title: l10n.agreementTitle, lead: l10n.agreementLead, diff --git a/apps/driver_new/lib/features/auth/view/login_page.dart b/apps/driver_new/lib/features/auth/view/login_page.dart index 64c4c38..48dee03 100644 --- a/apps/driver_new/lib/features/auth/view/login_page.dart +++ b/apps/driver_new/lib/features/auth/view/login_page.dart @@ -1,13 +1,21 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/di.dart'; + import '../../../core/session/session_cubit.dart'; + import '../cubit/login_cubit.dart'; + import '../cubit/login_state.dart'; + import 'agreement_view.dart'; + import 'otp_view.dart'; + import 'permission_view.dart'; + import 'phone_view.dart'; /// مضيف تدفّق الدخول: خطوة واحدة معروضة في كل لحظة، والانتقال بينها من diff --git a/apps/driver_new/lib/features/auth/view/otp_view.dart b/apps/driver_new/lib/features/auth/view/otp_view.dart index bc10912..de0d752 100644 --- a/apps/driver_new/lib/features/auth/view/otp_view.dart +++ b/apps/driver_new/lib/features/auth/view/otp_view.dart @@ -1,16 +1,26 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/design/tokens.dart'; + import '../../../core/design/tripz_colors.dart'; + import '../../../core/l10n/l10n.dart'; + +import '../../../core/ui/failure_text.dart'; + import '../../../core/ui/tripz_button.dart'; + import '../../../core/ui/tripz_otp_field.dart'; + import '../../../core/ui/tripz_scaffold.dart'; + import '../cubit/login_cubit.dart'; + import '../cubit/login_state.dart'; -import 'widgets/auth_failure_text.dart'; -import 'widgets/auth_header.dart'; + +import '../../../core/ui/form_header.dart'; class OtpView extends StatefulWidget { const OtpView({super.key}); @@ -44,7 +54,7 @@ class _OtpViewState extends State { child: ListView( children: [ const SizedBox(height: Space.xl), - AuthHeader( + FormHeader( icon: Icons.mark_email_read_outlined, title: l10n.otpTitle, lead: l10n.otpLead(state.phone), diff --git a/apps/driver_new/lib/features/auth/view/permission_view.dart b/apps/driver_new/lib/features/auth/view/permission_view.dart index f8f4b2e..449d77b 100644 --- a/apps/driver_new/lib/features/auth/view/permission_view.dart +++ b/apps/driver_new/lib/features/auth/view/permission_view.dart @@ -1,14 +1,22 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/design/tokens.dart'; + import '../../../core/l10n/l10n.dart'; + import '../../../core/ui/status_banner.dart'; + import '../../../core/ui/tripz_button.dart'; + import '../../../core/ui/tripz_scaffold.dart'; + import '../cubit/login_cubit.dart'; + import '../cubit/login_state.dart'; -import 'widgets/auth_header.dart'; + +import '../../../core/ui/form_header.dart'; /// البوابة الثانية: إذن الموقع. عند الرفض النهائي لا يبقى إلا فتح إعدادات /// النظام — والعودة منها يلتقطها `LoginPage` بمراقبة دورة الحياة. @@ -35,7 +43,7 @@ class PermissionView extends StatelessWidget { child: ListView( children: [ const SizedBox(height: Space.xl), - AuthHeader( + FormHeader( icon: Icons.my_location_rounded, title: l10n.permissionTitle, lead: l10n.permissionLead, diff --git a/apps/driver_new/lib/features/auth/view/phone_view.dart b/apps/driver_new/lib/features/auth/view/phone_view.dart index 8a7e7dd..5752888 100644 --- a/apps/driver_new/lib/features/auth/view/phone_view.dart +++ b/apps/driver_new/lib/features/auth/view/phone_view.dart @@ -1,15 +1,24 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/design/tokens.dart'; + import '../../../core/l10n/l10n.dart'; + +import '../../../core/ui/failure_text.dart'; + import '../../../core/ui/tripz_button.dart'; + import '../../../core/ui/tripz_scaffold.dart'; + import '../../../core/ui/tripz_text_field.dart'; + import '../cubit/login_cubit.dart'; + import '../cubit/login_state.dart'; -import 'widgets/auth_failure_text.dart'; -import 'widgets/auth_header.dart'; + +import '../../../core/ui/form_header.dart'; class PhoneView extends StatelessWidget { const PhoneView({super.key}); @@ -34,7 +43,7 @@ class PhoneView extends StatelessWidget { child: ListView( children: [ const SizedBox(height: Space.xl), - AuthHeader( + FormHeader( icon: Icons.phone_outlined, title: l10n.phoneTitle, lead: l10n.phoneLead, diff --git a/apps/driver_new/lib/features/auth/cubit/profile_cubit.dart b/apps/driver_new/lib/features/profile/cubit/profile_cubit.dart similarity index 88% rename from apps/driver_new/lib/features/auth/cubit/profile_cubit.dart rename to apps/driver_new/lib/features/profile/cubit/profile_cubit.dart index 6e16708..abeb9d8 100644 --- a/apps/driver_new/lib/features/auth/cubit/profile_cubit.dart +++ b/apps/driver_new/lib/features/profile/cubit/profile_cubit.dart @@ -1,10 +1,14 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/api/api_exception.dart'; -import '../../../core/ui/view_status.dart'; -import '../data/auth_repository.dart'; + +import '../../../core/api/api_failure.dart'; + import '../../../core/session/app_user.dart'; -import 'auth_failure.dart'; + +import '../../../core/ui/view_status.dart'; + +import '../data/profile_repository.dart'; class ProfileState { const ProfileState({ @@ -40,12 +44,12 @@ class ProfileState { class ProfileCubit extends Cubit { ProfileCubit(this._repo) : super(const ProfileState()); - final AuthRepository _repo; + final ProfileRepository _repo; Future save(String name) async { emit(state.copyWith(saving: true, clearFailure: true)); try { - final user = await _repo.updateProfile(name: name.trim()); + final user = await _repo.update(name: name.trim()); emit(state.copyWith(saving: false, user: user)); return user; } on ApiException catch (e) { diff --git a/apps/driver_new/lib/features/profile/data/profile_repository.dart b/apps/driver_new/lib/features/profile/data/profile_repository.dart new file mode 100644 index 0000000..758fbc3 --- /dev/null +++ b/apps/driver_new/lib/features/profile/data/profile_repository.dart @@ -0,0 +1,20 @@ +import '../../../core/api/api_client.dart'; +import '../../../core/session/app_user.dart'; + +/// `PATCH /users/me` — يقبل `{ name, language }` فقط (docs/38 §3). +/// +/// مستودع مستقلّ عن `AuthRepository` عمداً: تعديل الملف ليس مصادقة، وربطهما +/// كان يجعل ميزة الملف تستورد ميزة المصادقة — وهو ممنوع (docs/23 §1). +class ProfileRepository { + ProfileRepository(this._api); + + final ApiClient _api; + + Future update({String? name, String? language}) async { + final res = await _api.patch>( + '/users/me', + body: {'name': ?name, 'language': ?language}, + ); + return AppUser.fromJson(res); + } +} diff --git a/apps/driver_new/lib/features/profile/view/profile_edit_page.dart b/apps/driver_new/lib/features/profile/view/profile_edit_page.dart new file mode 100644 index 0000000..4fe1654 --- /dev/null +++ b/apps/driver_new/lib/features/profile/view/profile_edit_page.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../core/design/tokens.dart'; + +import '../../../core/design/tripz_colors.dart'; + +import '../../../core/di.dart'; + +import '../../../core/l10n/l10n.dart'; + +import '../../../core/session/session_cubit.dart'; + +import '../../../core/ui/tripz_button.dart'; + +import '../../../core/ui/tripz_scaffold.dart'; + +import '../../../core/ui/tripz_text_field.dart'; + +import '../cubit/profile_cubit.dart'; + +/// تعديل الاسم. **الرقم لا يُعدَّل**: هو هوية الحساب على الخادم، وتغييره +/// يعني حساباً آخر (docs/38 §1) — فيُعرض معطّلاً مع سبب واضح لا مخفياً. +class ProfileEditPage extends StatefulWidget { + const ProfileEditPage({super.key}); + + @override + State createState() => _ProfileEditPageState(); +} + +class _ProfileEditPageState extends State { + late final TextEditingController _name; + + @override + void initState() { + super.initState(); + _name = TextEditingController( + text: sl().state.user?.name ?? '', + ); + } + + @override + void dispose() { + _name.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return BlocProvider( + create: (_) => ProfileCubit(sl()), + child: BlocBuilder( + builder: (context, state) { + final user = context.watch().state.user; + return TripzScaffold( + title: l10n.profileEditTitle, + bottomAction: TripzButton.primary( + label: l10n.actionSave, + loading: state.saving, + onPressed: () => _save(context), + ), + child: ListView( + children: [ + TripzTextField( + controller: _name, + label: l10n.profileNameLabel, + enabled: !state.saving, + ), + const SizedBox(height: Space.md), + TripzTextField( + controller: TextEditingController(text: user?.phone ?? ''), + label: l10n.profilePhone, + enabled: false, + numeric: true, + ), + const SizedBox(height: Space.xxs), + Text( + l10n.profilePhoneLocked, + style: context.texts.bodySmall?.copyWith( + color: context.colors.onSurfaceVariant, + ), + ), + ], + ), + ); + }, + ), + ); + } + + Future _save(BuildContext context) async { + final l10n = context.l10n; + final messenger = ScaffoldMessenger.of(context); + final session = context.read(); + final user = await context.read().save(_name.text); + if (user == null) return; + session.onProfileCompleted(user); + messenger.showSnackBar(SnackBar(content: Text(l10n.profileSaved))); + } +} diff --git a/apps/rider_new/lib/features/auth/view/profile_page.dart b/apps/driver_new/lib/features/profile/view/profile_setup_page.dart similarity index 87% rename from apps/rider_new/lib/features/auth/view/profile_page.dart rename to apps/driver_new/lib/features/profile/view/profile_setup_page.dart index 9664276..a5c5ca4 100644 --- a/apps/rider_new/lib/features/auth/view/profile_page.dart +++ b/apps/driver_new/lib/features/profile/view/profile_setup_page.dart @@ -1,25 +1,35 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/design/tokens.dart'; -import '../../../core/di.dart'; -import '../../../core/l10n/l10n.dart'; -import '../../../core/ui/tripz_button.dart'; -import '../../../core/ui/tripz_scaffold.dart'; -import '../../../core/ui/tripz_text_field.dart'; -import '../cubit/profile_cubit.dart'; -import '../../../core/session/session_cubit.dart'; -import 'widgets/auth_failure_text.dart'; -import 'widgets/auth_header.dart'; -class ProfilePage extends StatefulWidget { - const ProfilePage({super.key}); +import '../../../core/di.dart'; + +import '../../../core/l10n/l10n.dart'; + +import '../../../core/session/session_cubit.dart'; + +import '../../../core/ui/failure_text.dart'; + +import '../../../core/ui/tripz_button.dart'; + +import '../../../core/ui/tripz_scaffold.dart'; + +import '../../../core/ui/tripz_text_field.dart'; + +import '../../../core/ui/form_header.dart'; + +import '../cubit/profile_cubit.dart'; + +class ProfileSetupPage extends StatefulWidget { + const ProfileSetupPage({super.key}); @override - State createState() => _ProfilePageState(); + State createState() => _ProfileSetupPageState(); } -class _ProfilePageState extends State { +class _ProfileSetupPageState extends State { final _name = TextEditingController(); String? _error; @@ -46,7 +56,7 @@ class _ProfilePageState extends State { child: ListView( children: [ const SizedBox(height: Space.xl), - AuthHeader( + FormHeader( icon: Icons.person_outline_rounded, title: l10n.profileTitle, lead: l10n.profileLead, diff --git a/apps/driver_new/lib/features/rating/cubit/rating_cubit.dart b/apps/driver_new/lib/features/rating/cubit/rating_cubit.dart index 05c47e0..d0d7364 100644 --- a/apps/driver_new/lib/features/rating/cubit/rating_cubit.dart +++ b/apps/driver_new/lib/features/rating/cubit/rating_cubit.dart @@ -1,7 +1,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/api/api_exception.dart'; + import '../data/rating_repository.dart'; + import 'rating_state.dart'; class RatingCubit extends Cubit { diff --git a/apps/driver_new/lib/features/rating/view/rating_page.dart b/apps/driver_new/lib/features/rating/view/rating_page.dart index 24074fc..9b11882 100644 --- a/apps/driver_new/lib/features/rating/view/rating_page.dart +++ b/apps/driver_new/lib/features/rating/view/rating_page.dart @@ -1,16 +1,27 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/design/tokens.dart'; + import '../../../core/design/tripz_colors.dart'; + import '../../../core/di.dart'; + import '../../../core/l10n/l10n.dart'; + import '../../../core/ui/tripz_button.dart'; + import '../../../core/ui/tripz_scaffold.dart'; + import '../../../core/ui/tripz_text_field.dart'; + import '../cubit/rating_cubit.dart'; + import '../cubit/rating_state.dart'; + import '../data/rating_repository.dart'; + import 'star_row.dart'; /// شاشة التقييم — **تُفرض عند فتح التطبيق** إن كانت هناك رحلة معلّقة diff --git a/apps/driver_new/lib/features/rating/view/star_row.dart b/apps/driver_new/lib/features/rating/view/star_row.dart index bd8f664..f118b82 100644 --- a/apps/driver_new/lib/features/rating/view/star_row.dart +++ b/apps/driver_new/lib/features/rating/view/star_row.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../../../core/design/tokens.dart'; + import '../../../core/design/tripz_colors.dart'; /// خمس نجوم — أهداف لمس ≥ 48 لأن الشاشة تُستعمل عادةً والمستخدم مستعجل. diff --git a/apps/driver_new/lib/features/settings/cubit/settings_cubit.dart b/apps/driver_new/lib/features/settings/cubit/settings_cubit.dart index 4875dff..b020ca3 100644 --- a/apps/driver_new/lib/features/settings/cubit/settings_cubit.dart +++ b/apps/driver_new/lib/features/settings/cubit/settings_cubit.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; + import 'package:shared_preferences/shared_preferences.dart'; import 'settings_state.dart'; diff --git a/apps/driver_new/lib/features/settings/cubit/settings_state.dart b/apps/driver_new/lib/features/settings/cubit/settings_state.dart index f841895..a0b4049 100644 --- a/apps/driver_new/lib/features/settings/cubit/settings_state.dart +++ b/apps/driver_new/lib/features/settings/cubit/settings_state.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; + import 'package:flutter/material.dart'; /// المظهر واللغة (docs/26 §4) — محفوظان محلياً، وليسا سرّاً فيذهبان إلى diff --git a/apps/driver_new/lib/features/settings/view/settings_page.dart b/apps/driver_new/lib/features/settings/view/settings_page.dart new file mode 100644 index 0000000..ce7d4c5 --- /dev/null +++ b/apps/driver_new/lib/features/settings/view/settings_page.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../core/design/tokens.dart'; + +import '../../../core/design/tripz_colors.dart'; + +import '../../../core/l10n/l10n.dart'; + +import '../../../core/ui/tripz_scaffold.dart'; + +import '../cubit/settings_cubit.dart'; + +import '../cubit/settings_state.dart'; + +/// المظهر واللغة. تبديل اللغة **يقلب الاتجاه كاملاً** بلا إعادة تشغيل — +/// `MaterialApp` يشتقّ RTL/LTR من الـlocale (docs/26 §4). +class SettingsPage extends StatelessWidget { + const SettingsPage({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return BlocBuilder( + builder: (context, state) { + final cubit = context.read(); + return TripzScaffold( + title: l10n.settingsTitle, + child: ListView( + children: [ + Text(l10n.settingsTheme, style: context.texts.titleMedium), + const SizedBox(height: Space.xs), + RadioGroup( + groupValue: state.themeMode, + onChanged: (v) => cubit.setThemeMode(v ?? ThemeMode.system), + child: Column( + children: [ + RadioListTile( + value: ThemeMode.system, + contentPadding: EdgeInsets.zero, + title: Text(l10n.settingsThemeSystem), + ), + RadioListTile( + value: ThemeMode.light, + contentPadding: EdgeInsets.zero, + title: Text(l10n.settingsThemeLight), + ), + RadioListTile( + value: ThemeMode.dark, + contentPadding: EdgeInsets.zero, + title: Text(l10n.settingsThemeDark), + ), + ], + ), + ), + const Divider(height: Space.xl), + Text(l10n.settingsLanguage, style: context.texts.titleMedium), + const SizedBox(height: Space.xs), + RadioGroup( + groupValue: state.locale.languageCode, + onChanged: (v) => cubit.setLocale(Locale(v ?? 'ar')), + child: Column( + children: [ + RadioListTile( + value: 'ar', + contentPadding: EdgeInsets.zero, + title: Text(l10n.settingsLanguageArabic), + ), + RadioListTile( + value: 'en', + contentPadding: EdgeInsets.zero, + title: Text(l10n.settingsLanguageEnglish), + ), + ], + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/apps/driver_new/lib/features/splash/view/splash_page.dart b/apps/driver_new/lib/features/splash/view/splash_page.dart index c8e5341..0a0f48b 100644 --- a/apps/driver_new/lib/features/splash/view/splash_page.dart +++ b/apps/driver_new/lib/features/splash/view/splash_page.dart @@ -1,13 +1,21 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/build_config.dart'; + import '../../../core/config.dart'; + import '../../../core/design/tokens.dart'; + import '../../../core/design/tripz_colors.dart'; + import '../../../core/l10n/l10n.dart'; + import '../../../core/session/session_cubit.dart'; +import '../../../core/tenant/tenant_cubit.dart'; + /// شاشة الإقلاع: تستعيد الجلسة ثم يتولّى الراوتر التوجيه. لا تنقّل من هنا — /// قرار الوجهة في مكان واحد (`core/router.dart`). class SplashPage extends StatefulWidget { @@ -23,7 +31,10 @@ class _SplashPageState extends State { super.initState(); // بعد أول إطار: `restore` قد يُصدر حالة فوراً، وإصدارها أثناء البناء خطأ. WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) context.read().restore(); + if (!mounted) return; + // إعداد المستأجر بلا توكن — يُجلب بالتوازي مع استعادة الجلسة لا بعدها. + context.read().load(); + context.read().restore(); }); } diff --git a/apps/driver_new/lib/features/support/view/support_page.dart b/apps/driver_new/lib/features/support/view/support_page.dart new file mode 100644 index 0000000..55bc86f --- /dev/null +++ b/apps/driver_new/lib/features/support/view/support_page.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +import '../../../core/build_config.dart'; + +import '../../../core/design/tokens.dart'; + +import '../../../core/design/tripz_colors.dart'; + +import '../../../core/l10n/l10n.dart'; + +import '../../../core/ui/tripz_card.dart'; + +import '../../../core/ui/tripz_scaffold.dart'; + +/// المساعدة و«عن التطبيق». محتوى محلّي عمداً: الخادم لا يوفّر نقطة أسئلة +/// شائعة، وربطها بنصّ من الشبكة يعني شاشة فارغة حين ينقطع الاتصال — وهو +/// بالضبط الوقت الذي يفتح فيه المستخدم المساعدة. +class SupportPage extends StatelessWidget { + const SupportPage({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return TripzScaffold( + title: l10n.supportTitle, + child: ListView( + children: [ + TripzCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.supportContact, style: context.texts.titleMedium), + const SizedBox(height: Space.xs), + Text( + l10n.supportWhatsapp, + style: context.texts.bodyMedium?.copyWith( + color: context.colors.onSurfaceVariant, + ), + ), + ], + ), + ), + const SizedBox(height: Space.md), + TripzCard( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(l10n.aboutVersion, style: context.texts.bodyMedium), + Text( + BuildConfig.appName, + style: context.texts.bodyMedium?.copyWith( + color: context.colors.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/apps/driver_new/lib/features/trip/cubit/history_cubit.dart b/apps/driver_new/lib/features/trip/cubit/history_cubit.dart new file mode 100644 index 0000000..1f7b35e --- /dev/null +++ b/apps/driver_new/lib/features/trip/cubit/history_cubit.dart @@ -0,0 +1,44 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../core/api/api_exception.dart'; + +import '../../../core/ui/view_status.dart'; + +import '../data/history_repository.dart'; + +import '../data/models/trip.dart'; + +class HistoryState { + const HistoryState({ + this.status = ViewStatus.idle, + this.trips = const [], + this.error, + }); + + final ViewStatus status; + final List trips; + final String? error; +} + +class HistoryCubit extends Cubit { + HistoryCubit(this._repo) : super(const HistoryState()); + + final HistoryRepository _repo; + + Future load() async { + emit(const HistoryState(status: ViewStatus.loading)); + try { + final trips = await _repo.trips(); + if (isClosed) return; + // قائمة فارغة ليست خطأ — لها حالتها الخاصة في TripzScaffold. + emit(HistoryState( + status: trips.isEmpty ? ViewStatus.empty : ViewStatus.success, + trips: trips, + )); + } on ApiException catch (e) { + if (!isClosed) { + emit(HistoryState(status: ViewStatus.error, error: e.message)); + } + } + } +} diff --git a/apps/driver_new/lib/features/trip/data/history_repository.dart b/apps/driver_new/lib/features/trip/data/history_repository.dart new file mode 100644 index 0000000..b57ccb6 --- /dev/null +++ b/apps/driver_new/lib/features/trip/data/history_repository.dart @@ -0,0 +1,19 @@ +import '../../../core/api/api_client.dart'; + +import 'models/trip.dart'; + +/// سجلّ الرحلات. لا يستورد ميزة الرحلة كمنطق — يعيد استعمال نموذجها فقط، +/// وهو الشكل الذي يرجّعه الخادم نفسه لكلا النقطتين. +class HistoryRepository { + HistoryRepository(this._api); + + final ApiClient _api; + + Future> trips() async { + final res = await _api.get>('/trips/mine'); + return res + .whereType>() + .map(Trip.fromJson) + .toList(growable: false); + } +} diff --git a/apps/driver_new/lib/features/trip/data/maps_repository.dart b/apps/driver_new/lib/features/trip/data/maps_repository.dart index f3dff78..292cc26 100644 --- a/apps/driver_new/lib/features/trip/data/maps_repository.dart +++ b/apps/driver_new/lib/features/trip/data/maps_repository.dart @@ -1,6 +1,9 @@ import '../../../core/api/antlaq_api.dart'; + import 'models/geo_point.dart'; + import 'models/place.dart'; + import 'models/route_info.dart'; /// كل الخرائط من map-saas مباشرة (قرار 2026-07-20). نقاط `/maps/*` في باك إند diff --git a/apps/driver_new/lib/features/trip/data/trip_repository.dart b/apps/driver_new/lib/features/trip/data/trip_repository.dart index 9b1afe9..f82d925 100644 --- a/apps/driver_new/lib/features/trip/data/trip_repository.dart +++ b/apps/driver_new/lib/features/trip/data/trip_repository.dart @@ -1,7 +1,11 @@ import '../../../core/api/api_client.dart'; + import 'models/fare_quote.dart'; + import 'models/geo_point.dart'; + import 'models/ride_type.dart'; + import 'models/trip.dart'; class TripRepository { diff --git a/apps/driver_new/lib/features/trip/view/duty_page.dart b/apps/driver_new/lib/features/trip/view/duty_page.dart index c40f062..0a20fe7 100644 --- a/apps/driver_new/lib/features/trip/view/duty_page.dart +++ b/apps/driver_new/lib/features/trip/view/duty_page.dart @@ -6,6 +6,7 @@ import '../../../core/design/tripz_colors.dart'; import '../../../core/di.dart'; import '../../../core/l10n/l10n.dart'; import '../../../core/ui/status_banner.dart'; +import '../../../core/ui/app_drawer.dart'; import '../cubit/duty_cubit.dart'; import '../cubit/duty_state.dart'; import '../data/models/geo_point.dart'; @@ -36,6 +37,7 @@ class _DutyPageState extends State { return BlocProvider.value( value: _cubit, child: Scaffold( + drawer: const AppDrawer(), body: Stack( children: [ // الخريطة معزولة عن بقية الحالة: نبضة موقع كل 25 متراً يجب ألّا @@ -50,6 +52,24 @@ class _DutyPageState extends State { padding: Space.page, child: Column( children: [ + Builder( + builder: (context) => Align( + alignment: AlignmentDirectional.topStart, + child: Material( + color: context.tripzColors.surfaceRaised, + shape: const CircleBorder(), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: Scaffold.of(context).openDrawer, + child: const Padding( + padding: EdgeInsets.all(Space.sm), + child: Icon(Icons.menu_rounded, size: Sizes.icon), + ), + ), + ), + ), + ), + const SizedBox(height: Space.xs), BlocSelector( selector: (s) => (online: s.isOnline, busy: s.busy), builder: (context, d) => DutyToggle( diff --git a/apps/driver_new/lib/features/trip/view/history_page.dart b/apps/driver_new/lib/features/trip/view/history_page.dart new file mode 100644 index 0000000..a62070b --- /dev/null +++ b/apps/driver_new/lib/features/trip/view/history_page.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../core/design/tokens.dart'; + +import '../../../core/design/tripz_colors.dart'; + +import '../../../core/design/typography.dart'; + +import '../../../core/di.dart'; + +import '../../../core/format/money.dart'; + +import '../../../core/l10n/l10n.dart'; + +import '../../../core/ui/empty_view.dart'; + +import '../../../core/ui/tripz_card.dart'; + +import '../../../core/ui/tripz_scaffold.dart'; + +import '../cubit/history_cubit.dart'; + +import '../data/models/trip.dart'; + +class HistoryPage extends StatefulWidget { + const HistoryPage({super.key}); + + @override + State createState() => _HistoryPageState(); +} + +class _HistoryPageState extends State { + late final HistoryCubit _cubit = sl()..load(); + + @override + void dispose() { + _cubit.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return BlocProvider.value( + value: _cubit, + child: BlocBuilder( + builder: (context, state) => TripzScaffold( + title: l10n.historyTitle, + status: state.status, + error: state.error, + onRetry: _cubit.load, + empty: EmptyView( + icon: Icons.route_outlined, + message: l10n.historyEmpty, + ), + child: ListView.separated( + itemCount: state.trips.length, + separatorBuilder: (context, i) => const SizedBox(height: Space.xs), + itemBuilder: (context, i) => _TripCard(trip: state.trips[i]), + ), + ), + ), + ); + } +} + +class _TripCard extends StatelessWidget { + const _TripCard({required this.trip}); + + final Trip trip; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final colors = context.tripzColors; + + final (label, color) = switch (trip.status) { + TripStatus.completed || TripStatus.paid => ( + l10n.historyStatusCompleted, + colors.success, + ), + TripStatus.cancelled || + TripStatus.expired || + TripStatus.noDrivers => + (l10n.historyStatusCancelled, colors.danger), + _ => (l10n.historyStatusLive, colors.info), + }; + + return TripzCard( + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ), + const SizedBox(width: Space.sm), + Expanded( + child: Text(label, style: context.texts.bodyMedium), + ), + Text( + Money.format(trip.priceForPassenger ?? trip.quotedFare, + trip.currency), + style: numericStyle(context.texts.titleSmall), + ), + ], + ), + ); + } +} diff --git a/apps/driver_new/lib/features/trip/view/widgets/driver_badge.dart b/apps/driver_new/lib/features/trip/view/widgets/driver_badge.dart index cabb7ac..d4493cd 100644 --- a/apps/driver_new/lib/features/trip/view/widgets/driver_badge.dart +++ b/apps/driver_new/lib/features/trip/view/widgets/driver_badge.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import '../../../../core/design/tokens.dart'; + import '../../../../core/design/tripz_colors.dart'; + import '../../../../core/design/typography.dart'; + import '../../data/models/trip.dart'; /// بطاقة السائق ولوحة المركبة — أهم ما يبحث عنه الراكب في الشارع، فيأخذ diff --git a/apps/driver_new/lib/features/trip/view/widgets/fare_summary.dart b/apps/driver_new/lib/features/trip/view/widgets/fare_summary.dart index baf4f83..70110ba 100644 --- a/apps/driver_new/lib/features/trip/view/widgets/fare_summary.dart +++ b/apps/driver_new/lib/features/trip/view/widgets/fare_summary.dart @@ -1,10 +1,15 @@ import 'package:flutter/material.dart'; import '../../../../core/design/tokens.dart'; + import '../../../../core/design/tripz_colors.dart'; + import '../../../../core/design/typography.dart'; + import '../../../../core/format/money.dart'; + import '../../../../core/l10n/l10n.dart'; + import '../../data/models/trip.dart'; /// ملخّص الأجرة عند الإنهاء. diff --git a/apps/driver_new/lib/features/trip/view/widgets/ride_map.dart b/apps/driver_new/lib/features/trip/view/widgets/ride_map.dart index 65956ec..de790f5 100644 --- a/apps/driver_new/lib/features/trip/view/widgets/ride_map.dart +++ b/apps/driver_new/lib/features/trip/view/widgets/ride_map.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; + import 'package:intaleq_maps/intaleq_maps.dart' as im; import '../../../../core/config.dart'; + import '../../../../core/design/tripz_colors.dart'; + import '../../data/models/geo_point.dart'; + import '../../data/models/route_info.dart'; /// الخريطة — **الطبقة الوحيدة التي تعرف `intaleq_maps`**. ما فوقها يتعامل diff --git a/apps/driver_new/lib/features/wallet/cubit/wallet_cubit.dart b/apps/driver_new/lib/features/wallet/cubit/wallet_cubit.dart index b8f743a..2e23664 100644 --- a/apps/driver_new/lib/features/wallet/cubit/wallet_cubit.dart +++ b/apps/driver_new/lib/features/wallet/cubit/wallet_cubit.dart @@ -1,8 +1,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/api/api_exception.dart'; + import '../../../core/ui/view_status.dart'; + import '../data/wallet_repository.dart'; + import 'wallet_state.dart'; class WalletCubit extends Cubit { diff --git a/apps/driver_new/lib/features/wallet/cubit/wallet_state.dart b/apps/driver_new/lib/features/wallet/cubit/wallet_state.dart index 5ddedc6..36f6287 100644 --- a/apps/driver_new/lib/features/wallet/cubit/wallet_state.dart +++ b/apps/driver_new/lib/features/wallet/cubit/wallet_state.dart @@ -1,7 +1,9 @@ import 'package:equatable/equatable.dart'; import '../../../core/ui/view_status.dart'; + import '../data/models/wallet.dart'; + import '../data/models/wallet_txn.dart'; enum WalletAction { none, toppingUp, topupFailed, topupDone } diff --git a/apps/driver_new/lib/features/wallet/data/wallet_repository.dart b/apps/driver_new/lib/features/wallet/data/wallet_repository.dart index 2f7a121..31972d0 100644 --- a/apps/driver_new/lib/features/wallet/data/wallet_repository.dart +++ b/apps/driver_new/lib/features/wallet/data/wallet_repository.dart @@ -1,5 +1,7 @@ import '../../../core/api/api_client.dart'; + import 'models/wallet.dart'; + import 'models/wallet_txn.dart'; class WalletRepository { diff --git a/apps/driver_new/lib/features/wallet/view/widgets/balance_card.dart b/apps/driver_new/lib/features/wallet/view/widgets/balance_card.dart index 469815a..06ce746 100644 --- a/apps/driver_new/lib/features/wallet/view/widgets/balance_card.dart +++ b/apps/driver_new/lib/features/wallet/view/widgets/balance_card.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import '../../../../core/design/tokens.dart'; + import '../../../../core/design/tripz_colors.dart'; + import '../../../../core/design/typography.dart'; + import '../../../../core/format/money.dart'; /// بطاقة الرصيد — الرقم هو بطل الشاشة، فيأخذ أكبر حجم وأعلى تباين. diff --git a/apps/driver_new/lib/features/wallet/view/widgets/txn_tile.dart b/apps/driver_new/lib/features/wallet/view/widgets/txn_tile.dart index dacd33f..3e06a1e 100644 --- a/apps/driver_new/lib/features/wallet/view/widgets/txn_tile.dart +++ b/apps/driver_new/lib/features/wallet/view/widgets/txn_tile.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; import '../../../../core/design/tokens.dart'; + import '../../../../core/design/tripz_colors.dart'; + import '../../../../core/design/typography.dart'; + import '../../../../core/format/money.dart'; + import '../../data/models/wallet_txn.dart'; class TxnTile extends StatelessWidget { diff --git a/apps/driver_new/lib/main.dart b/apps/driver_new/lib/main.dart index 955cded..042ab85 100644 --- a/apps/driver_new/lib/main.dart +++ b/apps/driver_new/lib/main.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import 'app.dart'; + import 'core/di.dart'; + import 'core/session/session_cubit.dart'; Future main() async { diff --git a/apps/driver_new/pubspec.lock b/apps/driver_new/pubspec.lock index 332ce46..45028b2 100644 --- a/apps/driver_new/pubspec.lock +++ b/apps/driver_new/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "6727cf2ced9b104abca9daa278380be2eca2b98ce33d4b46f11708e387dc6b4d" + url: "https://pub.dev" + source: hosted + version: "1.3.76" archive: dependency: transitive description: @@ -161,6 +169,54 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "9478ca6700c02d315c6aba37e206e612317f98bb4f335bb1cbd6e0ce67dcf764" + url: "https://pub.dev" + source: hosted + version: "4.13.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: e28f9afdcb5b0f0a8ea74ea3b322f5a7592c81cb45dd9d189913bdae08a2089a + url: "https://pub.dev" + source: hosted + version: "8.1.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: f471a288b0101a45567548322ac4a5ad31e3ecbf87a576dcc0e424e6a11e04ea + url: "https://pub.dev" + source: hosted + version: "3.10.0" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + sha256: "55cf1000dd72d229ebff3ce6a399bd35705a7675275478b31308afd90ac85751" + url: "https://pub.dev" + source: hosted + version: "16.5.0" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + sha256: e5fbb62bd23a27c983825277877ac91fdfdb6bfb31c6e5399700963ae586e339 + url: "https://pub.dev" + source: hosted + version: "4.9.3" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + sha256: "663d320e90a41fe57d651ec79df2bc831bf29e051bc0c2cd41faf3106eb412d9" + url: "https://pub.dev" + source: hosted + version: "4.2.4" fixnum: dependency: transitive description: diff --git a/apps/driver_new/pubspec.yaml b/apps/driver_new/pubspec.yaml index c764835..116e270 100644 --- a/apps/driver_new/pubspec.yaml +++ b/apps/driver_new/pubspec.yaml @@ -36,6 +36,10 @@ dependencies: geolocator: ^14.0.2 socket_io_client: ^3.0.2 + # م6: الإشعارات — الإعداد الأصلي (google-services) منقول مع الطبقة الأصلية + firebase_core: ^4.4.0 + firebase_messaging: ^16.1.1 + # م3: الأذونات وبصمة الجهاز permission_handler: ^12.0.1 device_info_plus: ^12.3.0 diff --git a/apps/rider_new/lib/app.dart b/apps/rider_new/lib/app.dart index 822088d..950cc6d 100644 --- a/apps/rider_new/lib/app.dart +++ b/apps/rider_new/lib/app.dart @@ -1,13 +1,23 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; + import 'package:flutter_localizations/flutter_localizations.dart'; import 'core/build_config.dart'; + import 'core/design/theme.dart'; + import 'core/di.dart'; + import 'core/l10n/l10n.dart'; + import 'core/router.dart'; + +import 'core/tenant/tenant_cubit.dart'; + import 'features/settings/cubit/settings_cubit.dart'; + import 'features/settings/cubit/settings_state.dart'; class TripzApp extends StatelessWidget { @@ -15,8 +25,12 @@ class TripzApp extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => sl(), + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: sl()), + // إعداد المستأجر فوق الشجرة: كل شاشة تسأله عن ميزاتها بلا تمرير. + BlocProvider.value(value: sl()), + ], child: BlocBuilder( builder: (context, settings) { return MaterialApp.router( diff --git a/apps/rider_new/lib/core/api/antlaq_api.dart b/apps/rider_new/lib/core/api/antlaq_api.dart index 9470484..3b4990c 100644 --- a/apps/rider_new/lib/core/api/antlaq_api.dart +++ b/apps/rider_new/lib/core/api/antlaq_api.dart @@ -1,6 +1,7 @@ import 'package:dio/dio.dart'; import '../config.dart'; + import 'api_exception.dart'; /// عميل map-saas — **منفصل تماماً** عن `ApiClient`. diff --git a/apps/rider_new/lib/core/api/api_client.dart b/apps/rider_new/lib/core/api/api_client.dart index 6f03250..04943f9 100644 --- a/apps/rider_new/lib/core/api/api_client.dart +++ b/apps/rider_new/lib/core/api/api_client.dart @@ -1,8 +1,11 @@ import 'package:dio/dio.dart'; import '../config.dart'; + import '../storage/token_store.dart'; + import 'api_exception.dart'; + import 'auth_interceptor.dart'; /// الواجهة الوحيدة للخادم. لا تُنشأ `Dio` في أي مكان آخر. diff --git a/apps/rider_new/lib/features/auth/cubit/auth_failure.dart b/apps/rider_new/lib/core/api/api_failure.dart similarity index 96% rename from apps/rider_new/lib/features/auth/cubit/auth_failure.dart rename to apps/rider_new/lib/core/api/api_failure.dart index 2efe08f..ba6d221 100644 --- a/apps/rider_new/lib/features/auth/cubit/auth_failure.dart +++ b/apps/rider_new/lib/core/api/api_failure.dart @@ -1,4 +1,4 @@ -import '../../../core/api/api_exception.dart'; +import 'api_exception.dart'; /// سبب الفشل كـ**رمز** لا كنصّ. /// diff --git a/apps/rider_new/lib/core/api/auth_interceptor.dart b/apps/rider_new/lib/core/api/auth_interceptor.dart index 4663e27..ebda130 100644 --- a/apps/rider_new/lib/core/api/auth_interceptor.dart +++ b/apps/rider_new/lib/core/api/auth_interceptor.dart @@ -3,7 +3,9 @@ import 'dart:async'; import 'package:dio/dio.dart'; import '../build_config.dart'; + import '../config.dart'; + import '../storage/token_store.dart'; /// يثبّت ترويسات كل طلب، ويجدّد التوكن **استباقياً** قبل انتهائه. diff --git a/apps/rider_new/lib/core/design/theme.dart b/apps/rider_new/lib/core/design/theme.dart index f375f8e..0b7204c 100644 --- a/apps/rider_new/lib/core/design/theme.dart +++ b/apps/rider_new/lib/core/design/theme.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import '../build_config.dart'; + import 'tokens.dart'; + import 'tripz_colors.dart'; + import 'typography.dart'; /// دالة بناء الثيم الوحيدة (docs/26 §4): مدخلان لا ثالث لهما. diff --git a/apps/rider_new/lib/core/di.dart b/apps/rider_new/lib/core/di.dart index 98679fc..c16f731 100644 --- a/apps/rider_new/lib/core/di.dart +++ b/apps/rider_new/lib/core/di.dart @@ -1,27 +1,56 @@ import 'package:device_info_plus/device_info_plus.dart'; + import 'package:flutter/foundation.dart'; + import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + import 'package:get_it/get_it.dart'; + import 'package:shared_preferences/shared_preferences.dart'; import '../features/auth/cubit/login_cubit.dart'; + import '../features/auth/data/auth_repository.dart'; +import '../features/profile/data/profile_repository.dart'; + import '../features/rating/cubit/rating_cubit.dart'; + import '../features/rating/data/rating_repository.dart'; + import '../features/settings/cubit/settings_cubit.dart'; + +import '../features/trip/cubit/history_cubit.dart'; + import '../features/trip/cubit/ride_cubit.dart'; -import '../features/wallet/cubit/wallet_cubit.dart'; -import '../features/wallet/data/wallet_repository.dart'; + +import '../features/trip/data/history_repository.dart'; + import '../features/trip/data/maps_repository.dart'; + import '../features/trip/data/trip_repository.dart'; + +import '../features/wallet/cubit/wallet_cubit.dart'; + +import '../features/wallet/data/wallet_repository.dart'; + import 'api/antlaq_api.dart'; + import 'api/api_client.dart'; -import 'location/location_service.dart'; -import 'realtime/realtime_service.dart'; -import 'session/session_cubit.dart'; + import 'build_config.dart'; + +import 'location/location_service.dart'; + +import 'notifications/push_service.dart'; + +import 'realtime/realtime_service.dart'; + +import 'session/session_cubit.dart'; + import 'storage/token_store.dart'; +import 'tenant/tenant_cubit.dart'; + final sl = GetIt.instance; /// نقطة الإقفال الوحيدة (docs/23 §5): الميزة المطفأة **لا تُسجَّل**، بدل @@ -51,7 +80,10 @@ Future setupInjector() async { ); sl.registerSingleton(AuthRepository(sl(), sl())); - sl.registerSingleton(SessionCubit(sl())); + sl.registerSingleton(ProfileRepository(sl())); + sl.registerSingleton( + SessionCubit(sl(), onAuthenticated: () => sl().register()), + ); // تدفّق الدخول قصير العمر: نسخة جديدة لكل دخول، لا نسخة واحدة أبدية. sl.registerFactory(() => LoginCubit(sl(), sl())); @@ -63,6 +95,12 @@ Future setupInjector() async { sl.registerSingleton(LocationService()); sl.registerSingleton(RealtimeService(tokens)); + sl.registerSingleton(TenantCubit(sl(), prefs)); + sl.registerSingleton(PushService(sl())); + + sl.registerSingleton(HistoryRepository(sl())); + sl.registerFactory(() => HistoryCubit(sl())); + sl.registerSingleton(RatingRepository(sl())); sl.registerFactory(() => RatingCubit(sl())); diff --git a/apps/rider_new/lib/core/l10n/app_ar.arb b/apps/rider_new/lib/core/l10n/app_ar.arb index f47527c..3fd557d 100644 --- a/apps/rider_new/lib/core/l10n/app_ar.arb +++ b/apps/rider_new/lib/core/l10n/app_ar.arb @@ -161,5 +161,32 @@ "payoutDone": "تم إرسال طلب السحب", "payoutFailed": "تعذّر إتمام السحب", "payoutInsufficient": "الرصيد لا يكفي", - "payoutHistory": "طلبات السحب" + "payoutHistory": "طلبات السحب", + "menuTitle": "القائمة", + "menuProfile": "حسابي", + "menuTrips": "رحلاتي", + "menuWallet": "المحفظة", + "menuEarnings": "أرباحي", + "menuSettings": "الإعدادات", + "menuSupport": "المساعدة", + "menuAbout": "عن التطبيق", + "menuLogout": "تسجيل الخروج", + "menuLogoutConfirm": "هل تريد الخروج من حسابك؟", + "historyTitle": "رحلاتي", + "historyEmpty": "لا رحلات بعد — اطلب أول رحلة لك", + "historyStatusCompleted": "مكتملة", + "historyStatusCancelled": "ملغاة", + "historyStatusLive": "جارية", + "profileEditTitle": "تعديل الحساب", + "profilePhone": "رقم الهاتف", + "profilePhoneLocked": "لا يمكن تغيير الرقم — هو هوية حسابك", + "profileSaved": "حُفظت التعديلات", + "supportTitle": "المساعدة", + "supportFaq": "الأسئلة الشائعة", + "supportContact": "تواصل معنا", + "supportWhatsapp": "مراسلة عبر واتساب", + "aboutVersion": "الإصدار", + "notifTitle": "الإشعارات", + "notifEnable": "تفعيل الإشعارات", + "notifDenied": "الإشعارات موقوفة من إعدادات النظام" } \ No newline at end of file diff --git a/apps/rider_new/lib/core/l10n/app_en.arb b/apps/rider_new/lib/core/l10n/app_en.arb index 3ef16c1..d84d7d7 100644 --- a/apps/rider_new/lib/core/l10n/app_en.arb +++ b/apps/rider_new/lib/core/l10n/app_en.arb @@ -161,5 +161,32 @@ "payoutDone": "Withdrawal request sent", "payoutFailed": "Withdrawal failed", "payoutInsufficient": "Insufficient balance", - "payoutHistory": "Withdrawal requests" + "payoutHistory": "Withdrawal requests", + "menuTitle": "Menu", + "menuProfile": "Account", + "menuTrips": "My trips", + "menuWallet": "Wallet", + "menuEarnings": "Earnings", + "menuSettings": "Settings", + "menuSupport": "Help", + "menuAbout": "About", + "menuLogout": "Sign out", + "menuLogoutConfirm": "Sign out of your account?", + "historyTitle": "My trips", + "historyEmpty": "No trips yet — request your first ride", + "historyStatusCompleted": "Completed", + "historyStatusCancelled": "Cancelled", + "historyStatusLive": "In progress", + "profileEditTitle": "Edit account", + "profilePhone": "Phone number", + "profilePhoneLocked": "Your number can't be changed — it identifies your account", + "profileSaved": "Changes saved", + "supportTitle": "Help", + "supportFaq": "FAQ", + "supportContact": "Contact us", + "supportWhatsapp": "Message on WhatsApp", + "aboutVersion": "Version", + "notifTitle": "Notifications", + "notifEnable": "Enable notifications", + "notifDenied": "Notifications are disabled in system settings" } \ No newline at end of file diff --git a/apps/rider_new/lib/core/notifications/push_service.dart b/apps/rider_new/lib/core/notifications/push_service.dart new file mode 100644 index 0000000..f9de3a7 --- /dev/null +++ b/apps/rider_new/lib/core/notifications/push_service.dart @@ -0,0 +1,66 @@ +import 'package:firebase_core/firebase_core.dart'; + +import 'package:firebase_messaging/firebase_messaging.dart'; + +import 'package:flutter/foundation.dart'; + +import '../api/api_client.dart'; + +import '../api/api_exception.dart'; + +/// إشعارات FCM — **الطبقة الوحيدة التي تعرف Firebase**. +/// +/// عقد الخادم نقطة واحدة: `POST /notifications/token { token, platform }` +/// (docs/38 §11). لا نقطة سرد إشعارات، فلا شاشة «صندوق إشعارات» تُبنى على +/// وهم — ما يصل يصل من النظام. +/// +/// الإعداد الأصلي جاهز مسبقاً: `google-services.json` و +/// `GoogleService-Info.plist` منقولان مع الطبقة الأصلية، و`build.gradle` +/// يطبّق `com.google.gms.google-services` أصلاً. +class PushService { + PushService(this._api); + + final ApiClient _api; + + /// يُنادى **بعد** الدخول: التسجيل يربط الجهاز بالمستخدم، فقبل الجلسة + /// لا معنى له. + Future register() async { + try { + await Firebase.initializeApp(); + final messaging = FirebaseMessaging.instance; + + final settings = await messaging.requestPermission(); + if (settings.authorizationStatus == AuthorizationStatus.denied) return; + + final token = await messaging.getToken(); + if (token != null) await _send(token); + + // الرمز يُدوَّر من Google دورياً — بلا هذا الاشتراك تتوقّف الإشعارات + // بصمت بعد أسابيع. + messaging.onTokenRefresh.listen(_send); + } catch (_) { + // غياب الإشعارات لا يمنع استعمال التطبيق — لا يُسقط الإقلاع. + } + } + + Future _send(String token) async { + try { + await _api.post>( + '/notifications/token', + body: { + 'token': token, + 'platform': defaultTargetPlatformName, + }, + ); + } on ApiException { + // يُعاد في الإقلاع التالي. + } + } +} + +/// المنصّة كما يتوقّعها الخادم — تُقرأ من المنصّة الفعلية لا من عَلَم بناء. +String get defaultTargetPlatformName => switch (defaultTargetPlatform) { + TargetPlatform.iOS => 'ios', + TargetPlatform.android => 'android', + _ => 'unknown', + }; diff --git a/apps/rider_new/lib/core/realtime/realtime_service.dart b/apps/rider_new/lib/core/realtime/realtime_service.dart index 48c8817..29b0022 100644 --- a/apps/rider_new/lib/core/realtime/realtime_service.dart +++ b/apps/rider_new/lib/core/realtime/realtime_service.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:socket_io_client/socket_io_client.dart' as io; import '../config.dart'; + import '../storage/token_store.dart'; /// أحداث الخادم كما هي في `backend-archive/src/realtime` (docs/38 §9). diff --git a/apps/rider_new/lib/core/router.dart b/apps/rider_new/lib/core/router.dart index 3fd091e..da10e4b 100644 --- a/apps/rider_new/lib/core/router.dart +++ b/apps/rider_new/lib/core/router.dart @@ -1,19 +1,37 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; + import 'package:go_router/go_router.dart'; -import 'session/session_cubit.dart'; -import 'session/session_state.dart'; import '../features/auth/view/login_page.dart'; -import '../features/auth/view/profile_page.dart'; + +import '../features/profile/view/profile_edit_page.dart'; + +import '../features/profile/view/profile_setup_page.dart'; + import '../features/rating/data/rating_repository.dart'; + import '../features/rating/view/rating_page.dart'; -import '../features/trip/view/ride_page.dart'; -import '../features/wallet/view/wallet_page.dart'; + +import '../features/settings/view/settings_page.dart'; + import '../features/splash/view/splash_page.dart'; + +import '../features/support/view/support_page.dart'; + +import '../features/trip/view/history_page.dart'; + +import '../features/trip/view/ride_page.dart'; + +import '../features/wallet/view/wallet_page.dart'; + import 'di.dart'; +import 'session/session_cubit.dart'; + +import 'session/session_state.dart'; + class Routes { const Routes._(); static const splash = '/'; @@ -22,6 +40,10 @@ class Routes { static const home = '/home'; static const rating = '/rating'; static const wallet = '/wallet'; + static const history = '/history'; + static const profileEdit = '/account'; + static const settings = '/settings'; + static const support = '/support'; } /// التوجيه يُشتقّ من `SessionCubit` وحده — لا `Navigator.push` بعد الدخول @@ -64,7 +86,7 @@ final appRouter = GoRouter( ), GoRoute( path: Routes.profile, - builder: (context, state) => const ProfilePage(), + builder: (context, state) => const ProfileSetupPage(), ), GoRoute( path: Routes.rating, @@ -83,6 +105,22 @@ final appRouter = GoRouter( path: Routes.wallet, builder: (context, state) => const WalletPage(), ), + GoRoute( + path: Routes.history, + builder: (context, state) => const HistoryPage(), + ), + GoRoute( + path: Routes.profileEdit, + builder: (context, state) => const ProfileEditPage(), + ), + GoRoute( + path: Routes.settings, + builder: (context, state) => const SettingsPage(), + ), + GoRoute( + path: Routes.support, + builder: (context, state) => const SupportPage(), + ), GoRoute( path: Routes.home, builder: (context, state) => const RidePage(), diff --git a/apps/rider_new/lib/core/session/session_cubit.dart b/apps/rider_new/lib/core/session/session_cubit.dart index 3a135c2..622377a 100644 --- a/apps/rider_new/lib/core/session/session_cubit.dart +++ b/apps/rider_new/lib/core/session/session_cubit.dart @@ -5,16 +5,24 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../api/api_exception.dart'; import 'app_user.dart'; + import 'session_source.dart'; + import 'session_state.dart'; /// حالة الجلسة على مستوى التطبيق — مصدر قرار التوجيه الوحيد /// (`core/router.dart` يستمع إليها). class SessionCubit extends Cubit { - SessionCubit(this._repo) : super(const SessionState()); + SessionCubit(this._repo, {Future Function()? onAuthenticated}) + : _onAuthenticated = onAuthenticated, + super(const SessionState()); final SessionSource _repo; + /// خطّاف يُنفَّذ مرة عند إثبات الجلسة — يبقي `core/session` جاهلاً بمن + /// يستمع إليه. + final Future Function()? _onAuthenticated; + /// تُستدعى مرة عند الإقلاع من شاشة الـsplash. Future restore() async { if (!_repo.hasSession) { @@ -60,8 +68,12 @@ class SessionCubit extends Cubit { user: user, pendingRating: state.pendingRating, )); - // التقييم المعلّق يُفحص بعد إثبات الجلسة لا قبلها. - if (!user.needsProfile) unawaited(_checkPendingRating()); + // بعد إثبات الجلسة: التقييم المعلّق، وتسجيل رمز الإشعارات (يربط الجهاز + // بالمستخدم، فلا معنى له قبل الدخول). + if (!user.needsProfile) { + unawaited(_checkPendingRating()); + unawaited(_onAuthenticated?.call() ?? Future.value()); + } } Future _checkPendingRating() async { diff --git a/apps/rider_new/lib/core/tenant/feature_gate.dart b/apps/rider_new/lib/core/tenant/feature_gate.dart new file mode 100644 index 0000000..9d51ae4 --- /dev/null +++ b/apps/rider_new/lib/core/tenant/feature_gate.dart @@ -0,0 +1,14 @@ +import 'package:flutter/widgets.dart'; + +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'tenant_cubit.dart'; + +extension FeatureGateX on BuildContext { + /// هل الميزة مفعّلة **لهذا المستأجر** وقت التشغيل؟ + /// + /// تُقرأ مع علم `Features` الثابت: الأول يقرّر ما بُني، وهذا ما يُعرض. + /// أيّهما `false` يُخفي الميزة. + bool tenantHas(String feature) => + watch().state.has(feature); +} diff --git a/apps/rider_new/lib/core/tenant/tenant_config.dart b/apps/rider_new/lib/core/tenant/tenant_config.dart new file mode 100644 index 0000000..daad1c4 --- /dev/null +++ b/apps/rider_new/lib/core/tenant/tenant_config.dart @@ -0,0 +1,79 @@ +import 'dart:convert'; + +import 'package:equatable/equatable.dart'; + +/// إعداد المستأجر من `GET /tenant/config/:slug` (شكل مُتحقَّق حيّاً، +/// docs/38 §10) — بلا توكن، فيُجلب قبل الدخول. +/// +/// **طبقتا الميزات**، ولكلٍّ دورها: +/// - `Features` (`const` في `build_config.dart`) يقرّر ما **يُبنى** في +/// الـbinary — إطفاؤه يحذف الكود فلا يُستخرج (docs/23 §5). +/// - هذا الإعداد يقرّر ما **يُعرض** ممّا بُني، حسب ما اشتراه المستأجر فعلاً. +/// الشاشة تتحقّق من الاثنتين: `Features.wallet && tenant.has('wallet')`. +class TenantConfig extends Equatable { + const TenantConfig({ + required this.slug, + required this.name, + required this.countryPack, + required this.plan, + required this.features, + }); + + final String slug; + final String name; + + /// يحدّد مزوّد الرسائل والعملة وصيغة الهاتف — `jo`/`eg`/`sy`. + final String countryPack; + + final String plan; + final Map features; + + static const empty = TenantConfig( + slug: '', + name: '', + countryPack: '', + plan: '', + features: {}, + ); + + /// الافتراض `false` عند غياب المفتاح: ميزة غير معلنة = غير مشتراة. + bool has(String feature) => features[feature] ?? false; + + factory TenantConfig.fromJson(Map json) { + final raw = json['features']; + return TenantConfig( + slug: json['slug'] as String? ?? '', + name: json['name'] as String? ?? '', + countryPack: json['countryPack'] as String? ?? '', + plan: json['plan'] as String? ?? '', + features: raw is Map + ? { + for (final e in raw.entries) + e.key.toString(): e.value == true, + } + : const {}, + ); + } + + Map toJson() => { + 'slug': slug, + 'name': name, + 'countryPack': countryPack, + 'plan': plan, + 'features': features, + }; + + String encode() => json.encode(toJson()); + + static TenantConfig? decode(String? raw) { + if (raw == null || raw.isEmpty) return null; + try { + return TenantConfig.fromJson(json.decode(raw) as Map); + } catch (_) { + return null; + } + } + + @override + List get props => [slug, name, countryPack, plan, features]; +} diff --git a/apps/rider_new/lib/core/tenant/tenant_cubit.dart b/apps/rider_new/lib/core/tenant/tenant_cubit.dart new file mode 100644 index 0000000..1ae1444 --- /dev/null +++ b/apps/rider_new/lib/core/tenant/tenant_cubit.dart @@ -0,0 +1,39 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:shared_preferences/shared_preferences.dart'; + +import '../api/api_client.dart'; + +import '../api/api_exception.dart'; + +import '../build_config.dart'; + +import 'tenant_config.dart'; + +/// يجلب إعداد المستأجر مرة عند الإقلاع، ويخزّنه محلياً. +/// +/// **النسخة المخزّنة تُعرض فوراً** ثم تُحدَّث من الشبكة: إقلاعٌ بلا إنترنت +/// يجب أن يعرض التطبيق بميزاته المعروفة آخر مرة، لا شاشة فارغة. +class TenantCubit extends Cubit { + TenantCubit(this._api, this._prefs) + : super(TenantConfig.decode(_prefs.getString(_key)) ?? TenantConfig.empty); + + final ApiClient _api; + final SharedPreferences _prefs; + + static const _key = 'tenant.config'; + + Future load() async { + try { + final res = await _api.get>( + '/tenant/config/${BuildConfig.tenantSlug}', + ); + final config = TenantConfig.fromJson(res); + emit(config); + await _prefs.setString(_key, config.encode()); + } on ApiException { + // الفشل يُبقي النسخة المخزّنة — لا يُطفئ ميزات المستخدم لأن الشبكة + // تعثّرت لحظة الإقلاع. + } + } +} diff --git a/apps/rider_new/lib/core/ui/app_drawer.dart b/apps/rider_new/lib/core/ui/app_drawer.dart new file mode 100644 index 0000000..a164663 --- /dev/null +++ b/apps/rider_new/lib/core/ui/app_drawer.dart @@ -0,0 +1,186 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:go_router/go_router.dart'; + +import '../build_config.dart'; + +import '../config.dart'; + +import '../design/tokens.dart'; + +import '../design/tripz_colors.dart'; + +import '../l10n/l10n.dart'; + +import '../router.dart'; + +import '../session/session_cubit.dart'; + +import '../session/session_state.dart'; + +import '../tenant/feature_gate.dart'; + +/// الدراور — نقطة تطبيق **طبقتَي الميزات** معاً (docs/23 §5 + docs/38 §10): +/// +/// * `Features.x` ثابت: يقرّر ما بُني في الـbinary. إطفاؤه يحذف الكود. +/// * `context.tenantHas('x')`: يقرّر ما يُعرض ممّا بُني، حسب اشتراك المستأجر. +/// +/// أيّهما `false` يُخفي المدخل — ولا يُكتب `if` ثانٍ في أي شاشة. +class AppDrawer extends StatelessWidget { + const AppDrawer({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final isDriver = AppConfig.appRole == 'driver'; + + return Drawer( + child: SafeArea( + child: Column( + children: [ + const _Header(), + const Divider(height: 1), + Expanded( + child: ListView( + padding: const EdgeInsets.symmetric(vertical: Space.xs), + children: [ + _Item( + icon: Icons.person_outline_rounded, + label: l10n.menuProfile, + route: Routes.profileEdit, + ), + _Item( + icon: Icons.route_outlined, + label: l10n.menuTrips, + route: Routes.history, + ), + if (Features.wallet && context.tenantHas('wallet')) + _Item( + icon: Icons.account_balance_wallet_outlined, + label: isDriver ? l10n.menuEarnings : l10n.menuWallet, + route: Routes.wallet, + ), + _Item( + icon: Icons.settings_outlined, + label: l10n.menuSettings, + route: Routes.settings, + ), + _Item( + icon: Icons.help_outline_rounded, + label: l10n.menuSupport, + route: Routes.support, + ), + ], + ), + ), + const Divider(height: 1), + ListTile( + leading: Icon( + Icons.logout_rounded, + color: context.tripzColors.danger, + ), + title: Text( + l10n.menuLogout, + style: TextStyle(color: context.tripzColors.danger), + ), + onTap: () => _confirmLogout(context), + ), + ], + ), + ), + ); + } + + Future _confirmLogout(BuildContext context) async { + final l10n = context.l10n; + final session = context.read(); + final ok = await showDialog( + context: context, + builder: (context) => AlertDialog( + content: Text(l10n.menuLogoutConfirm), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(l10n.actionCancel), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(l10n.menuLogout), + ), + ], + ), + ); + // الخروج يمرّ بالجلسة وحدها؛ الراوتر يعيد التوجيه من تلقائه. + if (ok == true) await session.logout(); + } +} + +class _Header extends StatelessWidget { + const _Header(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, session) { + final user = session.user; + return Padding( + padding: Space.page, + child: Row( + children: [ + CircleAvatar( + radius: 26, + backgroundColor: context.colors.primaryContainer, + child: Icon( + Icons.person_rounded, + color: context.colors.onPrimaryContainer, + ), + ), + const SizedBox(width: Space.sm), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + user?.name ?? BuildConfig.appName, + style: context.texts.titleMedium, + overflow: TextOverflow.ellipsis, + ), + Text( + user?.phone ?? '', + textDirection: TextDirection.ltr, + style: context.texts.bodySmall?.copyWith( + color: context.colors.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + } +} + +class _Item extends StatelessWidget { + const _Item({required this.icon, required this.label, required this.route}); + + final IconData icon; + final String label; + final String route; + + @override + Widget build(BuildContext context) { + return ListTile( + leading: Icon(icon), + title: Text(label), + onTap: () { + Navigator.of(context).pop(); + context.push(route); + }, + ); + } +} diff --git a/apps/rider_new/lib/core/ui/empty_view.dart b/apps/rider_new/lib/core/ui/empty_view.dart index bae9cdb..bcd69aa 100644 --- a/apps/rider_new/lib/core/ui/empty_view.dart +++ b/apps/rider_new/lib/core/ui/empty_view.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import '../design/tokens.dart'; + import '../design/tripz_colors.dart'; + import '../l10n/l10n.dart'; + import 'tripz_button.dart'; /// حالة الفراغ: رسالة **تدلّ على الفعل التالي** لا مجرّد «لا يوجد» diff --git a/apps/rider_new/lib/core/ui/error_view.dart b/apps/rider_new/lib/core/ui/error_view.dart index 0e0eb7c..e348218 100644 --- a/apps/rider_new/lib/core/ui/error_view.dart +++ b/apps/rider_new/lib/core/ui/error_view.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import '../design/tokens.dart'; + import '../design/tripz_colors.dart'; + import '../l10n/l10n.dart'; + import 'tripz_button.dart'; /// حالة الخطأ: رسالة **بالعربية** مع «أعد المحاولة» — لا شاشة صامتة diff --git a/apps/driver_new/lib/features/auth/view/widgets/auth_failure_text.dart b/apps/rider_new/lib/core/ui/failure_text.dart similarity index 89% rename from apps/driver_new/lib/features/auth/view/widgets/auth_failure_text.dart rename to apps/rider_new/lib/core/ui/failure_text.dart index 6936ea3..2b7cd48 100644 --- a/apps/driver_new/lib/features/auth/view/widgets/auth_failure_text.dart +++ b/apps/rider_new/lib/core/ui/failure_text.dart @@ -1,10 +1,14 @@ import 'package:flutter/material.dart'; -import '../../../../core/design/tokens.dart'; -import '../../../../core/design/tripz_colors.dart'; -import '../../../../core/l10n/l10n.dart'; -import '../../cubit/auth_failure.dart'; -import '../../cubit/phone_error.dart'; +import '../../features/auth/cubit/phone_error.dart'; + +import '../api/api_failure.dart'; + +import '../design/tokens.dart'; + +import '../design/tripz_colors.dart'; + +import '../l10n/l10n.dart'; /// النقطة **الوحيدة** التي تُترجَم فيها رموز الفشل إلى نصّ. /// الشاشات لا تكتب رسائل خطأ، والـCubit لا يعرف اللغة (راجع `auth_failure.dart`). diff --git a/apps/rider_new/lib/features/auth/view/widgets/auth_header.dart b/apps/rider_new/lib/core/ui/form_header.dart similarity index 88% rename from apps/rider_new/lib/features/auth/view/widgets/auth_header.dart rename to apps/rider_new/lib/core/ui/form_header.dart index 1de0d2d..201213d 100644 --- a/apps/rider_new/lib/features/auth/view/widgets/auth_header.dart +++ b/apps/rider_new/lib/core/ui/form_header.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; -import '../../../../core/design/tokens.dart'; -import '../../../../core/design/tripz_colors.dart'; +import '../design/tokens.dart'; + +import '../design/tripz_colors.dart'; /// ترويسة موحّدة لشاشات الدخول: أيقونة ثم عنوان ثم سطر شارح. /// «الشاشة مهمة واحدة» — سؤال واحد وزر أساسي واحد أسفلها (docs/26 §8.1). -class AuthHeader extends StatelessWidget { - const AuthHeader({ +class FormHeader extends StatelessWidget { + const FormHeader({ super.key, required this.icon, required this.title, diff --git a/apps/rider_new/lib/core/ui/skeleton.dart b/apps/rider_new/lib/core/ui/skeleton.dart index 9c5fddb..f72c366 100644 --- a/apps/rider_new/lib/core/ui/skeleton.dart +++ b/apps/rider_new/lib/core/ui/skeleton.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../design/tokens.dart'; + import '../design/tripz_colors.dart'; /// هيكل تحميل — **لا spinner فارغ** (docs/26 §8.4). diff --git a/apps/rider_new/lib/core/ui/status_banner.dart b/apps/rider_new/lib/core/ui/status_banner.dart index b2e3c9d..4007c18 100644 --- a/apps/rider_new/lib/core/ui/status_banner.dart +++ b/apps/rider_new/lib/core/ui/status_banner.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../design/tokens.dart'; + import '../design/tripz_colors.dart'; enum BannerTone { info, success, warning, danger } diff --git a/apps/rider_new/lib/core/ui/tripz_otp_field.dart b/apps/rider_new/lib/core/ui/tripz_otp_field.dart index 0779f88..ad1a22d 100644 --- a/apps/rider_new/lib/core/ui/tripz_otp_field.dart +++ b/apps/rider_new/lib/core/ui/tripz_otp_field.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; + import 'package:flutter/services.dart'; import '../design/tokens.dart'; + import '../design/typography.dart'; /// حقل رمز التحقّق — أربع خانات (طول رمز تريبز، docs/38 §2). diff --git a/apps/rider_new/lib/core/ui/tripz_scaffold.dart b/apps/rider_new/lib/core/ui/tripz_scaffold.dart index 8e5fdcf..9972c2b 100644 --- a/apps/rider_new/lib/core/ui/tripz_scaffold.dart +++ b/apps/rider_new/lib/core/ui/tripz_scaffold.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; import '../design/tokens.dart'; + import 'empty_view.dart'; + import 'error_view.dart'; + import 'skeleton.dart'; + import 'view_status.dart'; /// نقطة التنظيم المركزية (docs/26 §6). **ممنوع `Scaffold` خام في diff --git a/apps/rider_new/lib/core/ui/tripz_text_field.dart b/apps/rider_new/lib/core/ui/tripz_text_field.dart index b0e0acb..a070057 100644 --- a/apps/rider_new/lib/core/ui/tripz_text_field.dart +++ b/apps/rider_new/lib/core/ui/tripz_text_field.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; + import 'package:flutter/services.dart'; import '../design/tokens.dart'; + import '../design/typography.dart'; /// حقل الإدخال الموحّد (docs/26 §7): نصّ · هاتف · بحث. diff --git a/apps/rider_new/lib/features/auth/cubit/login_cubit.dart b/apps/rider_new/lib/features/auth/cubit/login_cubit.dart index 389a2d4..cf11776 100644 --- a/apps/rider_new/lib/features/auth/cubit/login_cubit.dart +++ b/apps/rider_new/lib/features/auth/cubit/login_cubit.dart @@ -1,14 +1,21 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; + import 'package:permission_handler/permission_handler.dart'; + import 'package:shared_preferences/shared_preferences.dart'; import '../../../core/api/api_exception.dart'; + +import '../../../core/api/api_failure.dart'; + import '../../../core/config.dart'; + import '../data/auth_repository.dart'; -import 'auth_failure.dart'; + import 'login_state.dart'; + import 'phone_error.dart'; /// تدفّق الدخول كاملاً: الشروط ← إذن الموقع ← الهاتف ← الرمز. diff --git a/apps/rider_new/lib/features/auth/cubit/login_state.dart b/apps/rider_new/lib/features/auth/cubit/login_state.dart index 9621f23..79f38ef 100644 --- a/apps/rider_new/lib/features/auth/cubit/login_state.dart +++ b/apps/rider_new/lib/features/auth/cubit/login_state.dart @@ -1,7 +1,9 @@ import 'package:equatable/equatable.dart'; +import '../../../core/api/api_failure.dart'; + import '../../../core/session/app_user.dart'; -import 'auth_failure.dart'; + import 'phone_error.dart'; /// خطوات الدخول بالترتيب الميداني المثبت في سيرو (docs/39 §2): بوابتان diff --git a/apps/rider_new/lib/features/auth/data/auth_repository.dart b/apps/rider_new/lib/features/auth/data/auth_repository.dart index e8ac180..3cc1ea3 100644 --- a/apps/rider_new/lib/features/auth/data/auth_repository.dart +++ b/apps/rider_new/lib/features/auth/data/auth_repository.dart @@ -1,8 +1,11 @@ import '../../../core/api/api_client.dart'; -import '../../../core/storage/token_store.dart'; + import '../../../core/session/app_user.dart'; + import '../../../core/session/session_source.dart'; +import '../../../core/storage/token_store.dart'; + /// كل نداءات المصادقة (docs/38 §2). الـCubit لا ينادي Dio مباشرة أبداً /// (docs/23 §3). class AuthRepository implements SessionSource { diff --git a/apps/rider_new/lib/features/auth/view/agreement_view.dart b/apps/rider_new/lib/features/auth/view/agreement_view.dart index 1f31675..8a77f9c 100644 --- a/apps/rider_new/lib/features/auth/view/agreement_view.dart +++ b/apps/rider_new/lib/features/auth/view/agreement_view.dart @@ -1,14 +1,22 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/design/tokens.dart'; + import '../../../core/design/tripz_colors.dart'; + import '../../../core/l10n/l10n.dart'; + import '../../../core/ui/tripz_button.dart'; + import '../../../core/ui/tripz_scaffold.dart'; + import '../cubit/login_cubit.dart'; + import '../cubit/login_state.dart'; -import 'widgets/auth_header.dart'; + +import '../../../core/ui/form_header.dart'; /// البوابة الأولى: شاشة كاملة لا حوار، وزر متابعة **معطّل حتى يُؤشَّر** /// (docs/39 §2). تُحفظ الموافقة فلا تتكرر. @@ -31,7 +39,7 @@ class AgreementView extends StatelessWidget { child: ListView( children: [ const SizedBox(height: Space.xl), - AuthHeader( + FormHeader( icon: Icons.policy_outlined, title: l10n.agreementTitle, lead: l10n.agreementLead, diff --git a/apps/rider_new/lib/features/auth/view/login_page.dart b/apps/rider_new/lib/features/auth/view/login_page.dart index 64c4c38..48dee03 100644 --- a/apps/rider_new/lib/features/auth/view/login_page.dart +++ b/apps/rider_new/lib/features/auth/view/login_page.dart @@ -1,13 +1,21 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/di.dart'; + import '../../../core/session/session_cubit.dart'; + import '../cubit/login_cubit.dart'; + import '../cubit/login_state.dart'; + import 'agreement_view.dart'; + import 'otp_view.dart'; + import 'permission_view.dart'; + import 'phone_view.dart'; /// مضيف تدفّق الدخول: خطوة واحدة معروضة في كل لحظة، والانتقال بينها من diff --git a/apps/rider_new/lib/features/auth/view/otp_view.dart b/apps/rider_new/lib/features/auth/view/otp_view.dart index bc10912..de0d752 100644 --- a/apps/rider_new/lib/features/auth/view/otp_view.dart +++ b/apps/rider_new/lib/features/auth/view/otp_view.dart @@ -1,16 +1,26 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/design/tokens.dart'; + import '../../../core/design/tripz_colors.dart'; + import '../../../core/l10n/l10n.dart'; + +import '../../../core/ui/failure_text.dart'; + import '../../../core/ui/tripz_button.dart'; + import '../../../core/ui/tripz_otp_field.dart'; + import '../../../core/ui/tripz_scaffold.dart'; + import '../cubit/login_cubit.dart'; + import '../cubit/login_state.dart'; -import 'widgets/auth_failure_text.dart'; -import 'widgets/auth_header.dart'; + +import '../../../core/ui/form_header.dart'; class OtpView extends StatefulWidget { const OtpView({super.key}); @@ -44,7 +54,7 @@ class _OtpViewState extends State { child: ListView( children: [ const SizedBox(height: Space.xl), - AuthHeader( + FormHeader( icon: Icons.mark_email_read_outlined, title: l10n.otpTitle, lead: l10n.otpLead(state.phone), diff --git a/apps/rider_new/lib/features/auth/view/permission_view.dart b/apps/rider_new/lib/features/auth/view/permission_view.dart index f8f4b2e..449d77b 100644 --- a/apps/rider_new/lib/features/auth/view/permission_view.dart +++ b/apps/rider_new/lib/features/auth/view/permission_view.dart @@ -1,14 +1,22 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/design/tokens.dart'; + import '../../../core/l10n/l10n.dart'; + import '../../../core/ui/status_banner.dart'; + import '../../../core/ui/tripz_button.dart'; + import '../../../core/ui/tripz_scaffold.dart'; + import '../cubit/login_cubit.dart'; + import '../cubit/login_state.dart'; -import 'widgets/auth_header.dart'; + +import '../../../core/ui/form_header.dart'; /// البوابة الثانية: إذن الموقع. عند الرفض النهائي لا يبقى إلا فتح إعدادات /// النظام — والعودة منها يلتقطها `LoginPage` بمراقبة دورة الحياة. @@ -35,7 +43,7 @@ class PermissionView extends StatelessWidget { child: ListView( children: [ const SizedBox(height: Space.xl), - AuthHeader( + FormHeader( icon: Icons.my_location_rounded, title: l10n.permissionTitle, lead: l10n.permissionLead, diff --git a/apps/rider_new/lib/features/auth/view/phone_view.dart b/apps/rider_new/lib/features/auth/view/phone_view.dart index 8a7e7dd..5752888 100644 --- a/apps/rider_new/lib/features/auth/view/phone_view.dart +++ b/apps/rider_new/lib/features/auth/view/phone_view.dart @@ -1,15 +1,24 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/design/tokens.dart'; + import '../../../core/l10n/l10n.dart'; + +import '../../../core/ui/failure_text.dart'; + import '../../../core/ui/tripz_button.dart'; + import '../../../core/ui/tripz_scaffold.dart'; + import '../../../core/ui/tripz_text_field.dart'; + import '../cubit/login_cubit.dart'; + import '../cubit/login_state.dart'; -import 'widgets/auth_failure_text.dart'; -import 'widgets/auth_header.dart'; + +import '../../../core/ui/form_header.dart'; class PhoneView extends StatelessWidget { const PhoneView({super.key}); @@ -34,7 +43,7 @@ class PhoneView extends StatelessWidget { child: ListView( children: [ const SizedBox(height: Space.xl), - AuthHeader( + FormHeader( icon: Icons.phone_outlined, title: l10n.phoneTitle, lead: l10n.phoneLead, diff --git a/apps/rider_new/lib/features/auth/cubit/profile_cubit.dart b/apps/rider_new/lib/features/profile/cubit/profile_cubit.dart similarity index 88% rename from apps/rider_new/lib/features/auth/cubit/profile_cubit.dart rename to apps/rider_new/lib/features/profile/cubit/profile_cubit.dart index 6e16708..abeb9d8 100644 --- a/apps/rider_new/lib/features/auth/cubit/profile_cubit.dart +++ b/apps/rider_new/lib/features/profile/cubit/profile_cubit.dart @@ -1,10 +1,14 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/api/api_exception.dart'; -import '../../../core/ui/view_status.dart'; -import '../data/auth_repository.dart'; + +import '../../../core/api/api_failure.dart'; + import '../../../core/session/app_user.dart'; -import 'auth_failure.dart'; + +import '../../../core/ui/view_status.dart'; + +import '../data/profile_repository.dart'; class ProfileState { const ProfileState({ @@ -40,12 +44,12 @@ class ProfileState { class ProfileCubit extends Cubit { ProfileCubit(this._repo) : super(const ProfileState()); - final AuthRepository _repo; + final ProfileRepository _repo; Future save(String name) async { emit(state.copyWith(saving: true, clearFailure: true)); try { - final user = await _repo.updateProfile(name: name.trim()); + final user = await _repo.update(name: name.trim()); emit(state.copyWith(saving: false, user: user)); return user; } on ApiException catch (e) { diff --git a/apps/rider_new/lib/features/profile/data/profile_repository.dart b/apps/rider_new/lib/features/profile/data/profile_repository.dart new file mode 100644 index 0000000..758fbc3 --- /dev/null +++ b/apps/rider_new/lib/features/profile/data/profile_repository.dart @@ -0,0 +1,20 @@ +import '../../../core/api/api_client.dart'; +import '../../../core/session/app_user.dart'; + +/// `PATCH /users/me` — يقبل `{ name, language }` فقط (docs/38 §3). +/// +/// مستودع مستقلّ عن `AuthRepository` عمداً: تعديل الملف ليس مصادقة، وربطهما +/// كان يجعل ميزة الملف تستورد ميزة المصادقة — وهو ممنوع (docs/23 §1). +class ProfileRepository { + ProfileRepository(this._api); + + final ApiClient _api; + + Future update({String? name, String? language}) async { + final res = await _api.patch>( + '/users/me', + body: {'name': ?name, 'language': ?language}, + ); + return AppUser.fromJson(res); + } +} diff --git a/apps/rider_new/lib/features/profile/view/profile_edit_page.dart b/apps/rider_new/lib/features/profile/view/profile_edit_page.dart new file mode 100644 index 0000000..4fe1654 --- /dev/null +++ b/apps/rider_new/lib/features/profile/view/profile_edit_page.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../core/design/tokens.dart'; + +import '../../../core/design/tripz_colors.dart'; + +import '../../../core/di.dart'; + +import '../../../core/l10n/l10n.dart'; + +import '../../../core/session/session_cubit.dart'; + +import '../../../core/ui/tripz_button.dart'; + +import '../../../core/ui/tripz_scaffold.dart'; + +import '../../../core/ui/tripz_text_field.dart'; + +import '../cubit/profile_cubit.dart'; + +/// تعديل الاسم. **الرقم لا يُعدَّل**: هو هوية الحساب على الخادم، وتغييره +/// يعني حساباً آخر (docs/38 §1) — فيُعرض معطّلاً مع سبب واضح لا مخفياً. +class ProfileEditPage extends StatefulWidget { + const ProfileEditPage({super.key}); + + @override + State createState() => _ProfileEditPageState(); +} + +class _ProfileEditPageState extends State { + late final TextEditingController _name; + + @override + void initState() { + super.initState(); + _name = TextEditingController( + text: sl().state.user?.name ?? '', + ); + } + + @override + void dispose() { + _name.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return BlocProvider( + create: (_) => ProfileCubit(sl()), + child: BlocBuilder( + builder: (context, state) { + final user = context.watch().state.user; + return TripzScaffold( + title: l10n.profileEditTitle, + bottomAction: TripzButton.primary( + label: l10n.actionSave, + loading: state.saving, + onPressed: () => _save(context), + ), + child: ListView( + children: [ + TripzTextField( + controller: _name, + label: l10n.profileNameLabel, + enabled: !state.saving, + ), + const SizedBox(height: Space.md), + TripzTextField( + controller: TextEditingController(text: user?.phone ?? ''), + label: l10n.profilePhone, + enabled: false, + numeric: true, + ), + const SizedBox(height: Space.xxs), + Text( + l10n.profilePhoneLocked, + style: context.texts.bodySmall?.copyWith( + color: context.colors.onSurfaceVariant, + ), + ), + ], + ), + ); + }, + ), + ); + } + + Future _save(BuildContext context) async { + final l10n = context.l10n; + final messenger = ScaffoldMessenger.of(context); + final session = context.read(); + final user = await context.read().save(_name.text); + if (user == null) return; + session.onProfileCompleted(user); + messenger.showSnackBar(SnackBar(content: Text(l10n.profileSaved))); + } +} diff --git a/apps/driver_new/lib/features/auth/view/profile_page.dart b/apps/rider_new/lib/features/profile/view/profile_setup_page.dart similarity index 87% rename from apps/driver_new/lib/features/auth/view/profile_page.dart rename to apps/rider_new/lib/features/profile/view/profile_setup_page.dart index 9664276..a5c5ca4 100644 --- a/apps/driver_new/lib/features/auth/view/profile_page.dart +++ b/apps/rider_new/lib/features/profile/view/profile_setup_page.dart @@ -1,25 +1,35 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/design/tokens.dart'; -import '../../../core/di.dart'; -import '../../../core/l10n/l10n.dart'; -import '../../../core/ui/tripz_button.dart'; -import '../../../core/ui/tripz_scaffold.dart'; -import '../../../core/ui/tripz_text_field.dart'; -import '../cubit/profile_cubit.dart'; -import '../../../core/session/session_cubit.dart'; -import 'widgets/auth_failure_text.dart'; -import 'widgets/auth_header.dart'; -class ProfilePage extends StatefulWidget { - const ProfilePage({super.key}); +import '../../../core/di.dart'; + +import '../../../core/l10n/l10n.dart'; + +import '../../../core/session/session_cubit.dart'; + +import '../../../core/ui/failure_text.dart'; + +import '../../../core/ui/tripz_button.dart'; + +import '../../../core/ui/tripz_scaffold.dart'; + +import '../../../core/ui/tripz_text_field.dart'; + +import '../../../core/ui/form_header.dart'; + +import '../cubit/profile_cubit.dart'; + +class ProfileSetupPage extends StatefulWidget { + const ProfileSetupPage({super.key}); @override - State createState() => _ProfilePageState(); + State createState() => _ProfileSetupPageState(); } -class _ProfilePageState extends State { +class _ProfileSetupPageState extends State { final _name = TextEditingController(); String? _error; @@ -46,7 +56,7 @@ class _ProfilePageState extends State { child: ListView( children: [ const SizedBox(height: Space.xl), - AuthHeader( + FormHeader( icon: Icons.person_outline_rounded, title: l10n.profileTitle, lead: l10n.profileLead, diff --git a/apps/rider_new/lib/features/rating/cubit/rating_cubit.dart b/apps/rider_new/lib/features/rating/cubit/rating_cubit.dart index 05c47e0..d0d7364 100644 --- a/apps/rider_new/lib/features/rating/cubit/rating_cubit.dart +++ b/apps/rider_new/lib/features/rating/cubit/rating_cubit.dart @@ -1,7 +1,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/api/api_exception.dart'; + import '../data/rating_repository.dart'; + import 'rating_state.dart'; class RatingCubit extends Cubit { diff --git a/apps/rider_new/lib/features/rating/view/rating_page.dart b/apps/rider_new/lib/features/rating/view/rating_page.dart index 24074fc..9b11882 100644 --- a/apps/rider_new/lib/features/rating/view/rating_page.dart +++ b/apps/rider_new/lib/features/rating/view/rating_page.dart @@ -1,16 +1,27 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/design/tokens.dart'; + import '../../../core/design/tripz_colors.dart'; + import '../../../core/di.dart'; + import '../../../core/l10n/l10n.dart'; + import '../../../core/ui/tripz_button.dart'; + import '../../../core/ui/tripz_scaffold.dart'; + import '../../../core/ui/tripz_text_field.dart'; + import '../cubit/rating_cubit.dart'; + import '../cubit/rating_state.dart'; + import '../data/rating_repository.dart'; + import 'star_row.dart'; /// شاشة التقييم — **تُفرض عند فتح التطبيق** إن كانت هناك رحلة معلّقة diff --git a/apps/rider_new/lib/features/rating/view/star_row.dart b/apps/rider_new/lib/features/rating/view/star_row.dart index bd8f664..f118b82 100644 --- a/apps/rider_new/lib/features/rating/view/star_row.dart +++ b/apps/rider_new/lib/features/rating/view/star_row.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../../../core/design/tokens.dart'; + import '../../../core/design/tripz_colors.dart'; /// خمس نجوم — أهداف لمس ≥ 48 لأن الشاشة تُستعمل عادةً والمستخدم مستعجل. diff --git a/apps/rider_new/lib/features/settings/cubit/settings_cubit.dart b/apps/rider_new/lib/features/settings/cubit/settings_cubit.dart index 4875dff..b020ca3 100644 --- a/apps/rider_new/lib/features/settings/cubit/settings_cubit.dart +++ b/apps/rider_new/lib/features/settings/cubit/settings_cubit.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; + import 'package:shared_preferences/shared_preferences.dart'; import 'settings_state.dart'; diff --git a/apps/rider_new/lib/features/settings/cubit/settings_state.dart b/apps/rider_new/lib/features/settings/cubit/settings_state.dart index f841895..a0b4049 100644 --- a/apps/rider_new/lib/features/settings/cubit/settings_state.dart +++ b/apps/rider_new/lib/features/settings/cubit/settings_state.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; + import 'package:flutter/material.dart'; /// المظهر واللغة (docs/26 §4) — محفوظان محلياً، وليسا سرّاً فيذهبان إلى diff --git a/apps/rider_new/lib/features/settings/view/settings_page.dart b/apps/rider_new/lib/features/settings/view/settings_page.dart new file mode 100644 index 0000000..ce7d4c5 --- /dev/null +++ b/apps/rider_new/lib/features/settings/view/settings_page.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../core/design/tokens.dart'; + +import '../../../core/design/tripz_colors.dart'; + +import '../../../core/l10n/l10n.dart'; + +import '../../../core/ui/tripz_scaffold.dart'; + +import '../cubit/settings_cubit.dart'; + +import '../cubit/settings_state.dart'; + +/// المظهر واللغة. تبديل اللغة **يقلب الاتجاه كاملاً** بلا إعادة تشغيل — +/// `MaterialApp` يشتقّ RTL/LTR من الـlocale (docs/26 §4). +class SettingsPage extends StatelessWidget { + const SettingsPage({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return BlocBuilder( + builder: (context, state) { + final cubit = context.read(); + return TripzScaffold( + title: l10n.settingsTitle, + child: ListView( + children: [ + Text(l10n.settingsTheme, style: context.texts.titleMedium), + const SizedBox(height: Space.xs), + RadioGroup( + groupValue: state.themeMode, + onChanged: (v) => cubit.setThemeMode(v ?? ThemeMode.system), + child: Column( + children: [ + RadioListTile( + value: ThemeMode.system, + contentPadding: EdgeInsets.zero, + title: Text(l10n.settingsThemeSystem), + ), + RadioListTile( + value: ThemeMode.light, + contentPadding: EdgeInsets.zero, + title: Text(l10n.settingsThemeLight), + ), + RadioListTile( + value: ThemeMode.dark, + contentPadding: EdgeInsets.zero, + title: Text(l10n.settingsThemeDark), + ), + ], + ), + ), + const Divider(height: Space.xl), + Text(l10n.settingsLanguage, style: context.texts.titleMedium), + const SizedBox(height: Space.xs), + RadioGroup( + groupValue: state.locale.languageCode, + onChanged: (v) => cubit.setLocale(Locale(v ?? 'ar')), + child: Column( + children: [ + RadioListTile( + value: 'ar', + contentPadding: EdgeInsets.zero, + title: Text(l10n.settingsLanguageArabic), + ), + RadioListTile( + value: 'en', + contentPadding: EdgeInsets.zero, + title: Text(l10n.settingsLanguageEnglish), + ), + ], + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/apps/rider_new/lib/features/splash/view/splash_page.dart b/apps/rider_new/lib/features/splash/view/splash_page.dart index c8e5341..0a0f48b 100644 --- a/apps/rider_new/lib/features/splash/view/splash_page.dart +++ b/apps/rider_new/lib/features/splash/view/splash_page.dart @@ -1,13 +1,21 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/build_config.dart'; + import '../../../core/config.dart'; + import '../../../core/design/tokens.dart'; + import '../../../core/design/tripz_colors.dart'; + import '../../../core/l10n/l10n.dart'; + import '../../../core/session/session_cubit.dart'; +import '../../../core/tenant/tenant_cubit.dart'; + /// شاشة الإقلاع: تستعيد الجلسة ثم يتولّى الراوتر التوجيه. لا تنقّل من هنا — /// قرار الوجهة في مكان واحد (`core/router.dart`). class SplashPage extends StatefulWidget { @@ -23,7 +31,10 @@ class _SplashPageState extends State { super.initState(); // بعد أول إطار: `restore` قد يُصدر حالة فوراً، وإصدارها أثناء البناء خطأ. WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) context.read().restore(); + if (!mounted) return; + // إعداد المستأجر بلا توكن — يُجلب بالتوازي مع استعادة الجلسة لا بعدها. + context.read().load(); + context.read().restore(); }); } diff --git a/apps/rider_new/lib/features/support/view/support_page.dart b/apps/rider_new/lib/features/support/view/support_page.dart new file mode 100644 index 0000000..55bc86f --- /dev/null +++ b/apps/rider_new/lib/features/support/view/support_page.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +import '../../../core/build_config.dart'; + +import '../../../core/design/tokens.dart'; + +import '../../../core/design/tripz_colors.dart'; + +import '../../../core/l10n/l10n.dart'; + +import '../../../core/ui/tripz_card.dart'; + +import '../../../core/ui/tripz_scaffold.dart'; + +/// المساعدة و«عن التطبيق». محتوى محلّي عمداً: الخادم لا يوفّر نقطة أسئلة +/// شائعة، وربطها بنصّ من الشبكة يعني شاشة فارغة حين ينقطع الاتصال — وهو +/// بالضبط الوقت الذي يفتح فيه المستخدم المساعدة. +class SupportPage extends StatelessWidget { + const SupportPage({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return TripzScaffold( + title: l10n.supportTitle, + child: ListView( + children: [ + TripzCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.supportContact, style: context.texts.titleMedium), + const SizedBox(height: Space.xs), + Text( + l10n.supportWhatsapp, + style: context.texts.bodyMedium?.copyWith( + color: context.colors.onSurfaceVariant, + ), + ), + ], + ), + ), + const SizedBox(height: Space.md), + TripzCard( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(l10n.aboutVersion, style: context.texts.bodyMedium), + Text( + BuildConfig.appName, + style: context.texts.bodyMedium?.copyWith( + color: context.colors.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/apps/rider_new/lib/features/trip/cubit/history_cubit.dart b/apps/rider_new/lib/features/trip/cubit/history_cubit.dart new file mode 100644 index 0000000..1f7b35e --- /dev/null +++ b/apps/rider_new/lib/features/trip/cubit/history_cubit.dart @@ -0,0 +1,44 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../core/api/api_exception.dart'; + +import '../../../core/ui/view_status.dart'; + +import '../data/history_repository.dart'; + +import '../data/models/trip.dart'; + +class HistoryState { + const HistoryState({ + this.status = ViewStatus.idle, + this.trips = const [], + this.error, + }); + + final ViewStatus status; + final List trips; + final String? error; +} + +class HistoryCubit extends Cubit { + HistoryCubit(this._repo) : super(const HistoryState()); + + final HistoryRepository _repo; + + Future load() async { + emit(const HistoryState(status: ViewStatus.loading)); + try { + final trips = await _repo.trips(); + if (isClosed) return; + // قائمة فارغة ليست خطأ — لها حالتها الخاصة في TripzScaffold. + emit(HistoryState( + status: trips.isEmpty ? ViewStatus.empty : ViewStatus.success, + trips: trips, + )); + } on ApiException catch (e) { + if (!isClosed) { + emit(HistoryState(status: ViewStatus.error, error: e.message)); + } + } + } +} diff --git a/apps/rider_new/lib/features/trip/cubit/ride_cubit.dart b/apps/rider_new/lib/features/trip/cubit/ride_cubit.dart index 47811fe..5705b6d 100644 --- a/apps/rider_new/lib/features/trip/cubit/ride_cubit.dart +++ b/apps/rider_new/lib/features/trip/cubit/ride_cubit.dart @@ -3,15 +3,25 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/api/api_exception.dart'; + import '../../../core/config.dart'; + import '../../../core/location/location_service.dart'; + import '../../../core/realtime/realtime_service.dart'; + import '../data/maps_repository.dart'; + import '../data/models/geo_point.dart'; + import '../data/models/place.dart'; + import '../data/models/ride_type.dart'; + import '../data/models/trip.dart'; + import '../data/trip_repository.dart'; + import 'ride_state.dart'; /// شاشة الراكب كاملة: التخطيط ← التسعيرة ← الطلب ← البحث ← الرحلة الجارية. diff --git a/apps/rider_new/lib/features/trip/cubit/ride_state.dart b/apps/rider_new/lib/features/trip/cubit/ride_state.dart index a90a9ba..6d27bbd 100644 --- a/apps/rider_new/lib/features/trip/cubit/ride_state.dart +++ b/apps/rider_new/lib/features/trip/cubit/ride_state.dart @@ -1,10 +1,15 @@ import 'package:equatable/equatable.dart'; import '../data/models/fare_quote.dart'; + import '../data/models/geo_point.dart'; + import '../data/models/place.dart'; + import '../data/models/ride_type.dart'; + import '../data/models/route_info.dart'; + import '../data/models/trip.dart'; /// مراحل شاشة الراكب. المرحلة تحدّد ما يُعرض على الخريطة وما في الورقة diff --git a/apps/rider_new/lib/features/trip/data/history_repository.dart b/apps/rider_new/lib/features/trip/data/history_repository.dart new file mode 100644 index 0000000..b57ccb6 --- /dev/null +++ b/apps/rider_new/lib/features/trip/data/history_repository.dart @@ -0,0 +1,19 @@ +import '../../../core/api/api_client.dart'; + +import 'models/trip.dart'; + +/// سجلّ الرحلات. لا يستورد ميزة الرحلة كمنطق — يعيد استعمال نموذجها فقط، +/// وهو الشكل الذي يرجّعه الخادم نفسه لكلا النقطتين. +class HistoryRepository { + HistoryRepository(this._api); + + final ApiClient _api; + + Future> trips() async { + final res = await _api.get>('/trips/mine'); + return res + .whereType>() + .map(Trip.fromJson) + .toList(growable: false); + } +} diff --git a/apps/rider_new/lib/features/trip/data/maps_repository.dart b/apps/rider_new/lib/features/trip/data/maps_repository.dart index f3dff78..292cc26 100644 --- a/apps/rider_new/lib/features/trip/data/maps_repository.dart +++ b/apps/rider_new/lib/features/trip/data/maps_repository.dart @@ -1,6 +1,9 @@ import '../../../core/api/antlaq_api.dart'; + import 'models/geo_point.dart'; + import 'models/place.dart'; + import 'models/route_info.dart'; /// كل الخرائط من map-saas مباشرة (قرار 2026-07-20). نقاط `/maps/*` في باك إند diff --git a/apps/rider_new/lib/features/trip/data/trip_repository.dart b/apps/rider_new/lib/features/trip/data/trip_repository.dart index 9b1afe9..f82d925 100644 --- a/apps/rider_new/lib/features/trip/data/trip_repository.dart +++ b/apps/rider_new/lib/features/trip/data/trip_repository.dart @@ -1,7 +1,11 @@ import '../../../core/api/api_client.dart'; + import 'models/fare_quote.dart'; + import 'models/geo_point.dart'; + import 'models/ride_type.dart'; + import 'models/trip.dart'; class TripRepository { diff --git a/apps/rider_new/lib/features/trip/view/history_page.dart b/apps/rider_new/lib/features/trip/view/history_page.dart new file mode 100644 index 0000000..a62070b --- /dev/null +++ b/apps/rider_new/lib/features/trip/view/history_page.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../core/design/tokens.dart'; + +import '../../../core/design/tripz_colors.dart'; + +import '../../../core/design/typography.dart'; + +import '../../../core/di.dart'; + +import '../../../core/format/money.dart'; + +import '../../../core/l10n/l10n.dart'; + +import '../../../core/ui/empty_view.dart'; + +import '../../../core/ui/tripz_card.dart'; + +import '../../../core/ui/tripz_scaffold.dart'; + +import '../cubit/history_cubit.dart'; + +import '../data/models/trip.dart'; + +class HistoryPage extends StatefulWidget { + const HistoryPage({super.key}); + + @override + State createState() => _HistoryPageState(); +} + +class _HistoryPageState extends State { + late final HistoryCubit _cubit = sl()..load(); + + @override + void dispose() { + _cubit.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return BlocProvider.value( + value: _cubit, + child: BlocBuilder( + builder: (context, state) => TripzScaffold( + title: l10n.historyTitle, + status: state.status, + error: state.error, + onRetry: _cubit.load, + empty: EmptyView( + icon: Icons.route_outlined, + message: l10n.historyEmpty, + ), + child: ListView.separated( + itemCount: state.trips.length, + separatorBuilder: (context, i) => const SizedBox(height: Space.xs), + itemBuilder: (context, i) => _TripCard(trip: state.trips[i]), + ), + ), + ), + ); + } +} + +class _TripCard extends StatelessWidget { + const _TripCard({required this.trip}); + + final Trip trip; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final colors = context.tripzColors; + + final (label, color) = switch (trip.status) { + TripStatus.completed || TripStatus.paid => ( + l10n.historyStatusCompleted, + colors.success, + ), + TripStatus.cancelled || + TripStatus.expired || + TripStatus.noDrivers => + (l10n.historyStatusCancelled, colors.danger), + _ => (l10n.historyStatusLive, colors.info), + }; + + return TripzCard( + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ), + const SizedBox(width: Space.sm), + Expanded( + child: Text(label, style: context.texts.bodyMedium), + ), + Text( + Money.format(trip.priceForPassenger ?? trip.quotedFare, + trip.currency), + style: numericStyle(context.texts.titleSmall), + ), + ], + ), + ); + } +} diff --git a/apps/rider_new/lib/features/trip/view/ride_page.dart b/apps/rider_new/lib/features/trip/view/ride_page.dart index 24efd5d..44a2e37 100644 --- a/apps/rider_new/lib/features/trip/view/ride_page.dart +++ b/apps/rider_new/lib/features/trip/view/ride_page.dart @@ -1,19 +1,35 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/design/tokens.dart'; + import '../../../core/design/tripz_colors.dart'; + import '../../../core/di.dart'; + import '../../../core/l10n/l10n.dart'; + +import '../../../core/ui/app_drawer.dart'; + import '../../../core/ui/tripz_button.dart'; + import '../cubit/ride_cubit.dart'; + import '../cubit/ride_state.dart'; + import '../data/models/geo_point.dart'; + import 'widgets/active_trip_sheet.dart'; + import 'widgets/center_pin.dart'; + import 'widgets/confirm_sheet.dart'; + import 'widgets/planner_sheet.dart'; + import 'widgets/ride_map.dart'; + import 'widgets/searching_sheet.dart'; /// شاشة الراكب: خريطة ثابتة + ورقة سفلية تتبدّل بالمرحلة (docs/39 §3). @@ -43,6 +59,7 @@ class _RidePageState extends State { return BlocProvider.value( value: _cubit, child: Scaffold( + drawer: const AppDrawer(), body: Stack( children: [ // ── الخريطة ──────────────────────────────────────────────── @@ -71,6 +88,19 @@ class _RidePageState extends State { picking ? const CenterPin() : const SizedBox.shrink(), ), + // زر القائمة — يطفو فوق الخريطة، ويحترم المنطقة الآمنة. + SafeArea( + child: Padding( + padding: Space.page, + child: Builder( + builder: (context) => _MapFab( + icon: Icons.menu_rounded, + onTap: Scaffold.of(context).openDrawer, + ), + ), + ), + ), + // ── الورقة السفلية ───────────────────────────────────────── Align( alignment: Alignment.bottomCenter, @@ -159,6 +189,33 @@ class _Sheet extends StatelessWidget { } } +/// زر دائري فوق الخريطة — سطح مرتفع كي يُقرأ فوق أي لون بلاطة. +class _MapFab extends StatelessWidget { + const _MapFab({required this.icon, required this.onTap}); + + final IconData icon; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Align( + alignment: AlignmentDirectional.topStart, + child: Material( + color: context.tripzColors.surfaceRaised, + shape: const CircleBorder(), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: onTap, + child: Padding( + padding: EdgeInsets.all(Space.sm), + child: Icon(icon, size: Sizes.icon), + ), + ), + ), + ); + } +} + class _Collapsed extends StatelessWidget { const _Collapsed(); diff --git a/apps/rider_new/lib/features/trip/view/widgets/active_trip_sheet.dart b/apps/rider_new/lib/features/trip/view/widgets/active_trip_sheet.dart index 0d235f9..b2b78ca 100644 --- a/apps/rider_new/lib/features/trip/view/widgets/active_trip_sheet.dart +++ b/apps/rider_new/lib/features/trip/view/widgets/active_trip_sheet.dart @@ -1,14 +1,23 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../core/design/tokens.dart'; + import '../../../../core/design/tripz_colors.dart'; + import '../../../../core/l10n/l10n.dart'; + import '../../../../core/ui/tripz_button.dart'; + import '../../cubit/ride_cubit.dart'; + import '../../data/models/trip.dart'; + import 'cancel_sheet.dart'; + import 'driver_badge.dart'; + import 'fare_summary.dart'; /// بطاقة الرحلة الجارية: حالة واضحة ثم بيانات السائق ثم فعل واحد. diff --git a/apps/rider_new/lib/features/trip/view/widgets/cancel_sheet.dart b/apps/rider_new/lib/features/trip/view/widgets/cancel_sheet.dart index 2b70745..184b6b2 100644 --- a/apps/rider_new/lib/features/trip/view/widgets/cancel_sheet.dart +++ b/apps/rider_new/lib/features/trip/view/widgets/cancel_sheet.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import '../../../../core/design/tokens.dart'; + import '../../../../core/l10n/l10n.dart'; + import '../../../../core/ui/tripz_button.dart'; + import '../../../../core/ui/tripz_sheet.dart'; /// الإلغاء لا يقع بضغطة واحدة: تأكيدٌ صريح مع سبب. diff --git a/apps/rider_new/lib/features/trip/view/widgets/center_pin.dart b/apps/rider_new/lib/features/trip/view/widgets/center_pin.dart index a047591..065871c 100644 --- a/apps/rider_new/lib/features/trip/view/widgets/center_pin.dart +++ b/apps/rider_new/lib/features/trip/view/widgets/center_pin.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../../../../core/design/tokens.dart'; + import '../../../../core/design/tripz_colors.dart'; /// دبّوس ثابت في منتصف الشاشة أثناء الاختيار من الخريطة. diff --git a/apps/rider_new/lib/features/trip/view/widgets/confirm_sheet.dart b/apps/rider_new/lib/features/trip/view/widgets/confirm_sheet.dart index d977676..bc4eecb 100644 --- a/apps/rider_new/lib/features/trip/view/widgets/confirm_sheet.dart +++ b/apps/rider_new/lib/features/trip/view/widgets/confirm_sheet.dart @@ -1,15 +1,25 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../core/design/tokens.dart'; + import '../../../../core/design/tripz_colors.dart'; + import '../../../../core/design/typography.dart'; + import '../../../../core/l10n/l10n.dart'; + import '../../../../core/ui/status_banner.dart'; + import '../../../../core/ui/tripz_button.dart'; + import '../../cubit/ride_cubit.dart'; + import '../../cubit/ride_state.dart'; + import 'ride_type_picker.dart'; + import 'route_timeline.dart'; /// التأكيد: نوع الرحلة والأجرة والمسافة والمدة — ثم زر واحد. diff --git a/apps/rider_new/lib/features/trip/view/widgets/driver_badge.dart b/apps/rider_new/lib/features/trip/view/widgets/driver_badge.dart index cabb7ac..d4493cd 100644 --- a/apps/rider_new/lib/features/trip/view/widgets/driver_badge.dart +++ b/apps/rider_new/lib/features/trip/view/widgets/driver_badge.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import '../../../../core/design/tokens.dart'; + import '../../../../core/design/tripz_colors.dart'; + import '../../../../core/design/typography.dart'; + import '../../data/models/trip.dart'; /// بطاقة السائق ولوحة المركبة — أهم ما يبحث عنه الراكب في الشارع، فيأخذ diff --git a/apps/rider_new/lib/features/trip/view/widgets/fare_summary.dart b/apps/rider_new/lib/features/trip/view/widgets/fare_summary.dart index baf4f83..70110ba 100644 --- a/apps/rider_new/lib/features/trip/view/widgets/fare_summary.dart +++ b/apps/rider_new/lib/features/trip/view/widgets/fare_summary.dart @@ -1,10 +1,15 @@ import 'package:flutter/material.dart'; import '../../../../core/design/tokens.dart'; + import '../../../../core/design/tripz_colors.dart'; + import '../../../../core/design/typography.dart'; + import '../../../../core/format/money.dart'; + import '../../../../core/l10n/l10n.dart'; + import '../../data/models/trip.dart'; /// ملخّص الأجرة عند الإنهاء. diff --git a/apps/rider_new/lib/features/trip/view/widgets/planner_sheet.dart b/apps/rider_new/lib/features/trip/view/widgets/planner_sheet.dart index a72fe32..4a9335a 100644 --- a/apps/rider_new/lib/features/trip/view/widgets/planner_sheet.dart +++ b/apps/rider_new/lib/features/trip/view/widgets/planner_sheet.dart @@ -1,12 +1,19 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../core/design/tokens.dart'; + import '../../../../core/l10n/l10n.dart'; + import '../../../../core/ui/tripz_button.dart'; + import '../../../../core/ui/tripz_text_field.dart'; + import '../../cubit/ride_cubit.dart'; + import '../../cubit/ride_state.dart'; + import 'route_timeline.dart'; /// المخطّط الموسّع بترتيبه المثبت ميدانياً (docs/39 §3): diff --git a/apps/rider_new/lib/features/trip/view/widgets/ride_map.dart b/apps/rider_new/lib/features/trip/view/widgets/ride_map.dart index 65956ec..de790f5 100644 --- a/apps/rider_new/lib/features/trip/view/widgets/ride_map.dart +++ b/apps/rider_new/lib/features/trip/view/widgets/ride_map.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; + import 'package:intaleq_maps/intaleq_maps.dart' as im; import '../../../../core/config.dart'; + import '../../../../core/design/tripz_colors.dart'; + import '../../data/models/geo_point.dart'; + import '../../data/models/route_info.dart'; /// الخريطة — **الطبقة الوحيدة التي تعرف `intaleq_maps`**. ما فوقها يتعامل diff --git a/apps/rider_new/lib/features/trip/view/widgets/ride_type_picker.dart b/apps/rider_new/lib/features/trip/view/widgets/ride_type_picker.dart index ed35bd1..b0122b5 100644 --- a/apps/rider_new/lib/features/trip/view/widgets/ride_type_picker.dart +++ b/apps/rider_new/lib/features/trip/view/widgets/ride_type_picker.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import '../../../../core/design/tokens.dart'; + import '../../../../core/design/tripz_colors.dart'; + import '../../data/models/ride_type.dart'; /// قائمة الأنواع تُبنى من `GET /ride-types` لا من قائمة مكتوبة (docs/38 §7). diff --git a/apps/rider_new/lib/features/trip/view/widgets/route_timeline.dart b/apps/rider_new/lib/features/trip/view/widgets/route_timeline.dart index e2c0894..07e87fd 100644 --- a/apps/rider_new/lib/features/trip/view/widgets/route_timeline.dart +++ b/apps/rider_new/lib/features/trip/view/widgets/route_timeline.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import '../../../../core/design/tokens.dart'; + import '../../../../core/design/tripz_colors.dart'; + import '../../../../core/l10n/l10n.dart'; /// الخط الزمني للمسار: المصدر ← المحطات ← الوجهة، بنقاط ملوّنة تطابق diff --git a/apps/rider_new/lib/features/trip/view/widgets/searching_sheet.dart b/apps/rider_new/lib/features/trip/view/widgets/searching_sheet.dart index d8e2830..44a32bb 100644 --- a/apps/rider_new/lib/features/trip/view/widgets/searching_sheet.dart +++ b/apps/rider_new/lib/features/trip/view/widgets/searching_sheet.dart @@ -1,10 +1,15 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../core/design/tokens.dart'; + import '../../../../core/design/tripz_colors.dart'; + import '../../../../core/l10n/l10n.dart'; + import '../../../../core/ui/tripz_button.dart'; + import '../../cubit/ride_cubit.dart'; /// نافذة البحث عن سائق. diff --git a/apps/rider_new/lib/features/wallet/cubit/wallet_cubit.dart b/apps/rider_new/lib/features/wallet/cubit/wallet_cubit.dart index b8f743a..2e23664 100644 --- a/apps/rider_new/lib/features/wallet/cubit/wallet_cubit.dart +++ b/apps/rider_new/lib/features/wallet/cubit/wallet_cubit.dart @@ -1,8 +1,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/api/api_exception.dart'; + import '../../../core/ui/view_status.dart'; + import '../data/wallet_repository.dart'; + import 'wallet_state.dart'; class WalletCubit extends Cubit { diff --git a/apps/rider_new/lib/features/wallet/cubit/wallet_state.dart b/apps/rider_new/lib/features/wallet/cubit/wallet_state.dart index 5ddedc6..36f6287 100644 --- a/apps/rider_new/lib/features/wallet/cubit/wallet_state.dart +++ b/apps/rider_new/lib/features/wallet/cubit/wallet_state.dart @@ -1,7 +1,9 @@ import 'package:equatable/equatable.dart'; import '../../../core/ui/view_status.dart'; + import '../data/models/wallet.dart'; + import '../data/models/wallet_txn.dart'; enum WalletAction { none, toppingUp, topupFailed, topupDone } diff --git a/apps/rider_new/lib/features/wallet/data/wallet_repository.dart b/apps/rider_new/lib/features/wallet/data/wallet_repository.dart index 2f7a121..31972d0 100644 --- a/apps/rider_new/lib/features/wallet/data/wallet_repository.dart +++ b/apps/rider_new/lib/features/wallet/data/wallet_repository.dart @@ -1,5 +1,7 @@ import '../../../core/api/api_client.dart'; + import 'models/wallet.dart'; + import 'models/wallet_txn.dart'; class WalletRepository { diff --git a/apps/rider_new/lib/features/wallet/view/wallet_page.dart b/apps/rider_new/lib/features/wallet/view/wallet_page.dart index 14208dd..24a2e7d 100644 --- a/apps/rider_new/lib/features/wallet/view/wallet_page.dart +++ b/apps/rider_new/lib/features/wallet/view/wallet_page.dart @@ -1,18 +1,31 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/design/tokens.dart'; + import '../../../core/design/tripz_colors.dart'; + import '../../../core/di.dart'; + import '../../../core/l10n/l10n.dart'; + import '../../../core/ui/empty_view.dart'; + import '../../../core/ui/tripz_button.dart'; + import '../../../core/ui/tripz_scaffold.dart'; + import '../../../core/ui/tripz_sheet.dart'; + import '../../../core/ui/tripz_text_field.dart'; + import '../cubit/wallet_cubit.dart'; + import '../cubit/wallet_state.dart'; + import 'widgets/balance_card.dart'; + import 'widgets/txn_tile.dart'; /// محفظة الراكب — **رصيدٌ دفعه ولم يستهلكه**، أي التزامٌ على المنصّة. diff --git a/apps/rider_new/lib/features/wallet/view/widgets/balance_card.dart b/apps/rider_new/lib/features/wallet/view/widgets/balance_card.dart index 469815a..06ce746 100644 --- a/apps/rider_new/lib/features/wallet/view/widgets/balance_card.dart +++ b/apps/rider_new/lib/features/wallet/view/widgets/balance_card.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import '../../../../core/design/tokens.dart'; + import '../../../../core/design/tripz_colors.dart'; + import '../../../../core/design/typography.dart'; + import '../../../../core/format/money.dart'; /// بطاقة الرصيد — الرقم هو بطل الشاشة، فيأخذ أكبر حجم وأعلى تباين. diff --git a/apps/rider_new/lib/features/wallet/view/widgets/txn_tile.dart b/apps/rider_new/lib/features/wallet/view/widgets/txn_tile.dart index dacd33f..3e06a1e 100644 --- a/apps/rider_new/lib/features/wallet/view/widgets/txn_tile.dart +++ b/apps/rider_new/lib/features/wallet/view/widgets/txn_tile.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; import '../../../../core/design/tokens.dart'; + import '../../../../core/design/tripz_colors.dart'; + import '../../../../core/design/typography.dart'; + import '../../../../core/format/money.dart'; + import '../../data/models/wallet_txn.dart'; class TxnTile extends StatelessWidget { diff --git a/apps/rider_new/lib/main.dart b/apps/rider_new/lib/main.dart index 955cded..042ab85 100644 --- a/apps/rider_new/lib/main.dart +++ b/apps/rider_new/lib/main.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import 'app.dart'; + import 'core/di.dart'; + import 'core/session/session_cubit.dart'; Future main() async { diff --git a/apps/rider_new/pubspec.lock b/apps/rider_new/pubspec.lock index 332ce46..45028b2 100644 --- a/apps/rider_new/pubspec.lock +++ b/apps/rider_new/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "6727cf2ced9b104abca9daa278380be2eca2b98ce33d4b46f11708e387dc6b4d" + url: "https://pub.dev" + source: hosted + version: "1.3.76" archive: dependency: transitive description: @@ -161,6 +169,54 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "9478ca6700c02d315c6aba37e206e612317f98bb4f335bb1cbd6e0ce67dcf764" + url: "https://pub.dev" + source: hosted + version: "4.13.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: e28f9afdcb5b0f0a8ea74ea3b322f5a7592c81cb45dd9d189913bdae08a2089a + url: "https://pub.dev" + source: hosted + version: "8.1.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: f471a288b0101a45567548322ac4a5ad31e3ecbf87a576dcc0e424e6a11e04ea + url: "https://pub.dev" + source: hosted + version: "3.10.0" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + sha256: "55cf1000dd72d229ebff3ce6a399bd35705a7675275478b31308afd90ac85751" + url: "https://pub.dev" + source: hosted + version: "16.5.0" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + sha256: e5fbb62bd23a27c983825277877ac91fdfdb6bfb31c6e5399700963ae586e339 + url: "https://pub.dev" + source: hosted + version: "4.9.3" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + sha256: "663d320e90a41fe57d651ec79df2bc831bf29e051bc0c2cd41faf3106eb412d9" + url: "https://pub.dev" + source: hosted + version: "4.2.4" fixnum: dependency: transitive description: diff --git a/apps/rider_new/pubspec.yaml b/apps/rider_new/pubspec.yaml index 56d1665..2ce373c 100644 --- a/apps/rider_new/pubspec.yaml +++ b/apps/rider_new/pubspec.yaml @@ -36,6 +36,10 @@ dependencies: geolocator: ^14.0.2 socket_io_client: ^3.0.2 + # م6: الإشعارات — الإعداد الأصلي (google-services) منقول مع الطبقة الأصلية + firebase_core: ^4.4.0 + firebase_messaging: ^16.1.1 + # م3: الأذونات وبصمة الجهاز permission_handler: ^12.0.1 device_info_plus: ^12.3.0 diff --git a/docs/37-newapps-cubit-plan.md b/docs/37-newapps-cubit-plan.md index e9aaef6..74e7a9c 100644 --- a/docs/37-newapps-cubit-plan.md +++ b/docs/37-newapps-cubit-plan.md @@ -277,7 +277,40 @@ 22. حالات الرحلة · التتبّع الحيّ · الإلغاء · الإنهاء · الأجرة · التقييم. 23. المحفظتان (محفظة الراكب = التزام، محفظة السائق = إيراد — لا تُدمجان). -### المرحلة 6 — الدراور والإضافات +### المرحلة 6 — الدراور والإضافات — **نُفّذت 2026-08-04** + +- **طبقتا الميزات مُطبَّقتان معاً** في `AppDrawer`: + `Features.wallet && context.tenantHas('wallet')`. الأولى `const` تقرّر ما + **يُبنى** في الـbinary (إطفاؤها يحذف الكود فلا يُستخرج — docs/23 §5)، + والثانية من `GET /tenant/config/:slug` تقرّر ما **يُعرض** ممّا بُني حسب + اشتراك المستأجر (docs/38 §10). هذا هو ربط lite/pro/max عملياً. +- `TenantCubit` يعرض **النسخة المخزّنة فوراً** ثم يحدّثها من الشبكة: إقلاعٌ + بلا إنترنت يعرض التطبيق بميزاته المعروفة آخر مرة، لا شاشة فارغة. +- الشاشات: الدراور · تعديل الحساب · سجلّ الرحلات · الإعدادات (مظهر + لغة) · + المساعدة. الرقم في تعديل الحساب **معطّل مع سبب معروض** لا مخفي: هو هوية + الحساب على الخادم وتغييره يعني حساباً آخر (docs/38 §1). +- الإشعارات: `PushService` معزول — **الملف الوحيد الذي يعرف Firebase**. + التسجيل يقع بعد إثبات الجلسة عبر خطّاف في `SessionCubit`، ويشترك في + `onTokenRefresh` (بدونه تتوقّف الإشعارات بصمت بعد أسابيع). الإعداد الأصلي + كان جاهزاً مسبقاً: `google-services.json` منقول و`build.gradle` يطبّق + `com.google.gms.google-services` أصلاً. + +**تصحيحات بنيوية أُجريت في هذه المرحلة** (كشفها فحص آلي لا قراءة): +`docs/23 §1` يمنع أن تستورد ميزةٌ من أخرى، وكان هناك خمس مخالفات: +1. `features/history` أُدمجت في `features/trip` — سجلّ الرحلات جزء من ميزة + الرحلة لا ميزة مستقلّة. +2. `AppDrawer` صعد إلى `core/ui/` — هيكل تطبيق لا ميزة. +3. `AuthFailure` صعد إلى `core/api/api_failure.dart`، ومترجمه إلى + `core/ui/failure_text.dart` — مشتركان بين المصادقة والملف. +4. `AuthHeader` صار `core/ui/form_header.dart`. +5. `ProfileRepository` مستقلّ عن `AuthRepository`: تعديل الملف ليس مصادقة. + +`flutter analyze` نظيف · الراكب 98 ملف/7,497 سطر · السائق 95 ملف/6,931 سطر. +فحوص آلية نظيفة: صفر استيراد بين ميزتين · `core` يستورد `features` من +`di.dart` و`router.dart` وحدهما · Firebase و`intaleq_maps` و`geolocator` و +`socket_io` كلٌّ معزول في ملفه. + +### (الأصل المعتمد للمرحلة 6) 24. الدراور · الملف الشخصي · الرحلات السابقة · الإشعارات · الدعم · اللغة. 25. الوحدات المدفوعة السبع (`docs/33`) خلف أعلام `const` حسب نموذج lite/pro/max — بناء مولَّد، لا فروع.