feat(apps): ثيم سيرو ومفتاح الدولة والعراق
## الثيم مقرَّب من سيرو (قرار المالك 2026-08-05) - البذرة صارت كحليّ سيرو #1A2340 بدل الأزرق العام، والثانوي #8C9CF8 - fromSeed وحدها تنتج من بذرة داكنة أسطحاً باهتة، فثُبّت الأساسي والثانوي والأسطح صراحةً وتُرك الباقي للمشتقّ — شكل سيرو مع تباين M3 - الأدوار الدلالية بأرقام سيرو الميدانية: نجاح #22C55E · تحذير #F59E0B · خطأ #EF4444، وبطاقة الليلي #2C2C2E ## مفتاح الدولة عاد core/ui/phone_field.dart — النمط المجرَّب في سيرو، لكن بلا intl_phone_field: تلك الحزمة تحمل 240 دولة ونحتاج أربعاً. CountryCode صريح ومقصور على أسواقنا. - الاختيار من ورقة سفلية لا قائمة منسدلة: أهداف لمس أكبر وأربع دول لا مئتان - الترتيب يبدأ بحزمة دولة المستأجر — الأردنيّ يرى الأردن أولاً بلا بحث - الرقم يُرسل كاملاً بالمفتاح بلا + (962790000001) كما يقبله الخادم - التحقّق صار بطول الدولة لا برقم ثابت: jo/sy تسع خانات، eg/iq عشر ## العراق - CountryCode.iraq (+964) في قائمة الهاتف - قنوات دفعه: زين كاش · كي كارد · نقد ⚠️ الباك إند لا يعرف حزمة `iq`: default-tariffs.ts فيه jo/sy/eg فقط، فمستأجر عراقي يُزوَّد بلا أي تعرفة. موثَّق في الكود — وهو شغل خادم يسبق أي إطلاق عراقي. flutter analyze نظيف في التطبيقين. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3a8caf7b27
commit
e22cc5c7e4
@@ -1,23 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
|
||||||
import 'core/build_config.dart';
|
import 'core/build_config.dart';
|
||||||
|
|
||||||
import 'core/design/theme.dart';
|
import 'core/design/theme.dart';
|
||||||
|
|
||||||
import 'core/di.dart';
|
import 'core/di.dart';
|
||||||
|
|
||||||
import 'core/l10n/l10n.dart';
|
import 'core/l10n/l10n.dart';
|
||||||
|
|
||||||
import 'core/router.dart';
|
import 'core/router.dart';
|
||||||
|
|
||||||
import 'core/tenant/tenant_cubit.dart';
|
import 'core/tenant/tenant_cubit.dart';
|
||||||
|
|
||||||
import 'features/settings/cubit/settings_cubit.dart';
|
import 'features/settings/cubit/settings_cubit.dart';
|
||||||
|
|
||||||
import 'features/settings/cubit/settings_state.dart';
|
import 'features/settings/cubit/settings_state.dart';
|
||||||
|
|
||||||
class TripzApp extends StatelessWidget {
|
class TripzApp extends StatelessWidget {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
import '../config.dart';
|
import '../config.dart';
|
||||||
|
|
||||||
import 'api_exception.dart';
|
import 'api_exception.dart';
|
||||||
|
|
||||||
/// عميل map-saas — **منفصل تماماً** عن `ApiClient`.
|
/// عميل map-saas — **منفصل تماماً** عن `ApiClient`.
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
import '../config.dart';
|
import '../config.dart';
|
||||||
|
|
||||||
import '../storage/token_store.dart';
|
import '../storage/token_store.dart';
|
||||||
|
|
||||||
import 'api_exception.dart';
|
import 'api_exception.dart';
|
||||||
|
|
||||||
import 'auth_interceptor.dart';
|
import 'auth_interceptor.dart';
|
||||||
|
|
||||||
/// الواجهة الوحيدة للخادم. لا تُنشأ `Dio` في أي مكان آخر.
|
/// الواجهة الوحيدة للخادم. لا تُنشأ `Dio` في أي مكان آخر.
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import 'dart:async';
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
import '../build_config.dart';
|
import '../build_config.dart';
|
||||||
|
|
||||||
import '../config.dart';
|
import '../config.dart';
|
||||||
|
|
||||||
import '../storage/token_store.dart';
|
import '../storage/token_store.dart';
|
||||||
|
|
||||||
/// يثبّت ترويسات كل طلب، ويجدّد التوكن **استباقياً** قبل انتهائه.
|
/// يثبّت ترويسات كل طلب، ويجدّد التوكن **استباقياً** قبل انتهائه.
|
||||||
|
|||||||
@@ -12,7 +12,13 @@ class BuildConfig {
|
|||||||
static const String tenantSlug = 'siro';
|
static const String tenantSlug = 'siro';
|
||||||
|
|
||||||
/// اللون البذرة — منه تُشتقّ `ColorScheme` للوضعين.
|
/// اللون البذرة — منه تُشتقّ `ColorScheme` للوضعين.
|
||||||
static const Color seedColor = Color(0xFF1F6FEB);
|
///
|
||||||
|
/// كحليّ سيرو (`#1A2340`) بقرار المالك 2026-08-05: الهوية البصرية تقترب من
|
||||||
|
/// سيرو المجرَّب ميدانياً بدل الأزرق العام.
|
||||||
|
static const Color seedColor = Color(0xFF1A2340);
|
||||||
|
|
||||||
|
/// اللون الثانوي — بنفسجي فاتح، يُستعمل للتمييز والحالات النشطة.
|
||||||
|
static const Color accentColor = Color(0xFF8C9CF8);
|
||||||
|
|
||||||
static const String logoAsset = 'assets/images/logo.png';
|
static const String logoAsset = 'assets/images/logo.png';
|
||||||
static const String splashLogoAsset = 'assets/images/splash_logo.png';
|
static const String splashLogoAsset = 'assets/images/splash_logo.png';
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../build_config.dart';
|
import '../build_config.dart';
|
||||||
|
|
||||||
import 'tokens.dart';
|
import 'tokens.dart';
|
||||||
|
|
||||||
import 'tripz_colors.dart';
|
import 'tripz_colors.dart';
|
||||||
|
|
||||||
import 'typography.dart';
|
import 'typography.dart';
|
||||||
|
|
||||||
/// دالة بناء الثيم الوحيدة (docs/26 §4): مدخلان لا ثالث لهما.
|
/// دالة بناء الثيم الوحيدة (docs/26 §4): مدخلان لا ثالث لهما.
|
||||||
@@ -13,9 +10,32 @@ import 'typography.dart';
|
|||||||
/// الاتجاه (RTL/LTR) **لا يُفرض هنا** — `MaterialApp` يشتقّه من الـlocale
|
/// الاتجاه (RTL/LTR) **لا يُفرض هنا** — `MaterialApp` يشتقّه من الـlocale
|
||||||
/// وحده. فرض `Directionality.rtl` يكسر الإنجليزية (docs/26 §4).
|
/// وحده. فرض `Directionality.rtl` يكسر الإنجليزية (docs/26 §4).
|
||||||
ThemeData buildTheme(Brightness brightness, Locale locale) {
|
ThemeData buildTheme(Brightness brightness, Locale locale) {
|
||||||
|
// البذرة الكحلية داكنة جداً، و`fromSeed` وحدها تنتج منها أسطحاً باهتة.
|
||||||
|
// نثبّت الأساسي والثانوي صراحةً ونترك للمشتقّ باقي الأدوار — فيبقى الشكل
|
||||||
|
// قريباً من سيرو مع ضمان التباين الذي يوفّره M3.
|
||||||
final scheme = ColorScheme.fromSeed(
|
final scheme = ColorScheme.fromSeed(
|
||||||
seedColor: BuildConfig.seedColor,
|
seedColor: BuildConfig.seedColor,
|
||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
|
).copyWith(
|
||||||
|
primary: brightness == Brightness.dark
|
||||||
|
? BuildConfig.accentColor
|
||||||
|
: BuildConfig.seedColor,
|
||||||
|
onPrimary: brightness == Brightness.dark
|
||||||
|
? BuildConfig.seedColor
|
||||||
|
: Colors.white,
|
||||||
|
secondary: BuildConfig.accentColor,
|
||||||
|
surface: brightness == Brightness.dark
|
||||||
|
? const Color(0xFF161618)
|
||||||
|
: const Color(0xFFF8FAFC),
|
||||||
|
onSurface: brightness == Brightness.dark
|
||||||
|
? Colors.white
|
||||||
|
: const Color(0xFF0F172A),
|
||||||
|
onSurfaceVariant: brightness == Brightness.dark
|
||||||
|
? const Color(0xFFB0B3B8)
|
||||||
|
: const Color(0xFF64748B),
|
||||||
|
outlineVariant: brightness == Brightness.dark
|
||||||
|
? const Color(0xFF3A3A3C)
|
||||||
|
: const Color(0xFFE2E8F0),
|
||||||
);
|
);
|
||||||
final isDark = brightness == Brightness.dark;
|
final isDark = brightness == Brightness.dark;
|
||||||
final texts = buildTextTheme(brightness, locale);
|
final texts = buildTextTheme(brightness, locale);
|
||||||
|
|||||||
@@ -40,35 +40,37 @@ class TripzColors extends ThemeExtension<TripzColors> {
|
|||||||
/// لون هيكل التحميل (skeleton) — لا spinner فارغ (docs/26 §8.4).
|
/// لون هيكل التحميل (skeleton) — لا spinner فارغ (docs/26 §8.4).
|
||||||
final Color skeleton;
|
final Color skeleton;
|
||||||
|
|
||||||
|
// الأرقام من نظام سيرو الميداني: نجاح `#22C55E` · تحذير `#F59E0B` ·
|
||||||
|
// خطأ `#EF4444` — مألوفة لمستخدمي سيرو ومجرَّبة على شاشات الشمس.
|
||||||
static const light = TripzColors(
|
static const light = TripzColors(
|
||||||
success: Color(0xFF1B8A5A),
|
success: Color(0xFF22C55E),
|
||||||
onSuccess: Color(0xFFFFFFFF),
|
onSuccess: Color(0xFFFFFFFF),
|
||||||
warning: Color(0xFFB4690E),
|
warning: Color(0xFFF59E0B),
|
||||||
onWarning: Color(0xFFFFFFFF),
|
onWarning: Color(0xFF2A1A00),
|
||||||
danger: Color(0xFFC5303B),
|
danger: Color(0xFFEF4444),
|
||||||
onDanger: Color(0xFFFFFFFF),
|
onDanger: Color(0xFFFFFFFF),
|
||||||
info: Color(0xFF2563A8),
|
info: Color(0xFF8C9CF8),
|
||||||
surfaceRaised: Color(0xFFFFFFFF),
|
surfaceRaised: Color(0xFFFFFFFF),
|
||||||
mapRoute: Color(0xFF1F6FEB),
|
mapRoute: Color(0xFF1A2340),
|
||||||
mapOrigin: Color(0xFF1B8A5A),
|
mapOrigin: Color(0xFF22C55E),
|
||||||
mapDestination: Color(0xFFC5303B),
|
mapDestination: Color(0xFFEF4444),
|
||||||
skeleton: Color(0xFFE6E8EC),
|
skeleton: Color(0xFFE2E8F0),
|
||||||
);
|
);
|
||||||
|
|
||||||
static const dark = TripzColors(
|
static const dark = TripzColors(
|
||||||
success: Color(0xFF4ECB8D),
|
success: Color(0xFF4ADE80),
|
||||||
onSuccess: Color(0xFF00281A),
|
onSuccess: Color(0xFF00281A),
|
||||||
warning: Color(0xFFE9A63B),
|
warning: Color(0xFFFBBF24),
|
||||||
onWarning: Color(0xFF2A1A00),
|
onWarning: Color(0xFF2A1A00),
|
||||||
danger: Color(0xFFF2707A),
|
danger: Color(0xFFF87171),
|
||||||
onDanger: Color(0xFF33000A),
|
onDanger: Color(0xFF33000A),
|
||||||
info: Color(0xFF7FB2F0),
|
info: Color(0xFF8C9CF8),
|
||||||
// ليس أسود صرفاً — رمادي داكن متدرّج.
|
// ليس أسود صرفاً — رمادي داكن متدرّج (بطاقة سيرو `#2C2C2E`).
|
||||||
surfaceRaised: Color(0xFF20232A),
|
surfaceRaised: Color(0xFF2C2C2E),
|
||||||
mapRoute: Color(0xFF5B9BFF),
|
mapRoute: Color(0xFF8C9CF8),
|
||||||
mapOrigin: Color(0xFF4ECB8D),
|
mapOrigin: Color(0xFF4ADE80),
|
||||||
mapDestination: Color(0xFFF2707A),
|
mapDestination: Color(0xFFF87171),
|
||||||
skeleton: Color(0xFF2C3038),
|
skeleton: Color(0xFF3A3A3C),
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
|
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import '../api/api_client.dart';
|
import '../api/api_client.dart';
|
||||||
|
|
||||||
import '../api/api_exception.dart';
|
import '../api/api_exception.dart';
|
||||||
|
|
||||||
/// إشعارات FCM — **الطبقة الوحيدة التي تعرف Firebase**.
|
/// إشعارات FCM — **الطبقة الوحيدة التي تعرف Firebase**.
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/// مفاتيح الدول المدعومة.
|
||||||
|
///
|
||||||
|
/// حزمة `intl_phone_field` التي يستعملها سيرو تحمل **240 دولة** ولا نحتاج
|
||||||
|
/// منها إلا أربعاً، وتجرّ معها أصولاً وتبعيّات. القائمة هنا صريحة ومقصورة
|
||||||
|
/// على أسواقنا — وتتوسّع بسطر واحد.
|
||||||
|
///
|
||||||
|
/// ترتيب العرض يبدأ بحزمة دولة المستأجر (`countryPack`)، فالمشغّل الأردني
|
||||||
|
/// يرى الأردن أولاً بلا بحث.
|
||||||
|
enum CountryCode {
|
||||||
|
jordan('jo', '962', 'الأردن', 'Jordan', '🇯🇴', 9),
|
||||||
|
syria('sy', '963', 'سوريا', 'Syria', '🇸🇾', 9),
|
||||||
|
egypt('eg', '20', 'مصر', 'Egypt', '🇪🇬', 10),
|
||||||
|
iraq('iq', '964', 'العراق', 'Iraq', '🇮🇶', 10);
|
||||||
|
|
||||||
|
const CountryCode(
|
||||||
|
this.pack,
|
||||||
|
this.dial,
|
||||||
|
this.nameAr,
|
||||||
|
this.nameEn,
|
||||||
|
this.flag,
|
||||||
|
this.nationalLength,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// يطابق `countryPack` في إعداد المستأجر.
|
||||||
|
final String pack;
|
||||||
|
|
||||||
|
/// المفتاح الدولي بلا `+`.
|
||||||
|
final String dial;
|
||||||
|
|
||||||
|
final String nameAr;
|
||||||
|
final String nameEn;
|
||||||
|
final String flag;
|
||||||
|
|
||||||
|
/// طول الرقم الوطني **بلا الصفر البادئ** — أساس التحقّق المحلّي.
|
||||||
|
final int nationalLength;
|
||||||
|
|
||||||
|
String nameFor(String languageCode) =>
|
||||||
|
languageCode == 'ar' ? nameAr : nameEn;
|
||||||
|
|
||||||
|
static CountryCode fromPack(String pack) =>
|
||||||
|
values.firstWhere((c) => c.pack == pack, orElse: () => jordan);
|
||||||
|
|
||||||
|
/// المستأجر أولاً ثم الباقي — لا ترتيب أبجدي يدفن سوق المشغّل للأسفل.
|
||||||
|
static List<CountryCode> orderedFor(String pack) {
|
||||||
|
final first = fromPack(pack);
|
||||||
|
return [first, ...values.where((c) => c != first)];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,8 @@ enum PaymentChannel {
|
|||||||
mtn('mtn_cash', 'MTN Cash', 'إم تي إن كاش'),
|
mtn('mtn_cash', 'MTN Cash', 'إم تي إن كاش'),
|
||||||
shamCash('sham_cash', 'Sham Cash', 'شام كاش'),
|
shamCash('sham_cash', 'Sham Cash', 'شام كاش'),
|
||||||
paymob('paymob', 'PayMob', 'باي موب'),
|
paymob('paymob', 'PayMob', 'باي موب'),
|
||||||
|
zainCash('zain_cash', 'ZainCash', 'زين كاش'),
|
||||||
|
qiCard('qi_card', 'Qi Card', 'كي كارد'),
|
||||||
cash('cash', 'Cash', 'نقداً');
|
cash('cash', 'Cash', 'نقداً');
|
||||||
|
|
||||||
const PaymentChannel(this.wire, this.nameEn, this.nameAr);
|
const PaymentChannel(this.wire, this.nameEn, this.nameAr);
|
||||||
@@ -38,6 +40,14 @@ enum PaymentChannel {
|
|||||||
PaymentChannel.cash,
|
PaymentChannel.cash,
|
||||||
],
|
],
|
||||||
'eg' => const [PaymentChannel.paymob, PaymentChannel.cash],
|
'eg' => const [PaymentChannel.paymob, PaymentChannel.cash],
|
||||||
|
// العراق: المحافظ الأشيع محلياً. ⚠️ الباك إند لا يعرف حزمة `iq` بعد
|
||||||
|
// (`default-tariffs.ts` فيه jo/sy/eg فقط) — مستأجر عراقي يُزوَّد بلا
|
||||||
|
// تعرفة، وهذا شغل خادم يسبق أي إطلاق عراقي.
|
||||||
|
'iq' => const [
|
||||||
|
PaymentChannel.zainCash,
|
||||||
|
PaymentChannel.qiCard,
|
||||||
|
PaymentChannel.cash,
|
||||||
|
],
|
||||||
// حزمة غير معروفة: النقد وحده — لا نعرض قناة قد لا تعمل في سوقها.
|
// حزمة غير معروفة: النقد وحده — لا نعرض قناة قد لا تعمل في سوقها.
|
||||||
_ => const [PaymentChannel.cash],
|
_ => const [PaymentChannel.cash],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,11 +3,8 @@ import 'dart:async';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../api/api_exception.dart';
|
import '../api/api_exception.dart';
|
||||||
|
|
||||||
import 'app_user.dart';
|
import 'app_user.dart';
|
||||||
|
|
||||||
import 'session_source.dart';
|
import 'session_source.dart';
|
||||||
|
|
||||||
import 'session_state.dart';
|
import 'session_state.dart';
|
||||||
|
|
||||||
/// حالة الجلسة على مستوى التطبيق — مصدر قرار التوجيه الوحيد
|
/// حالة الجلسة على مستوى التطبيق — مصدر قرار التوجيه الوحيد
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import 'tenant_cubit.dart';
|
import 'tenant_cubit.dart';
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../api/api_client.dart';
|
import '../api/api_client.dart';
|
||||||
|
|
||||||
import '../api/api_exception.dart';
|
import '../api/api_exception.dart';
|
||||||
|
|
||||||
import '../build_config.dart';
|
import '../build_config.dart';
|
||||||
|
|
||||||
import 'tenant_config.dart';
|
import 'tenant_config.dart';
|
||||||
|
|
||||||
/// يجلب إعداد المستأجر مرة عند الإقلاع، ويخزّنه محلياً.
|
/// يجلب إعداد المستأجر مرة عند الإقلاع، ويخزّنه محلياً.
|
||||||
|
|||||||
@@ -1,25 +1,15 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../build_config.dart';
|
import '../build_config.dart';
|
||||||
|
|
||||||
import '../config.dart';
|
import '../config.dart';
|
||||||
|
|
||||||
import '../design/tokens.dart';
|
import '../design/tokens.dart';
|
||||||
|
|
||||||
import '../design/tripz_colors.dart';
|
import '../design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
|
|
||||||
import '../router.dart';
|
import '../router.dart';
|
||||||
|
|
||||||
import '../session/session_cubit.dart';
|
import '../session/session_cubit.dart';
|
||||||
|
|
||||||
import '../session/session_state.dart';
|
import '../session/session_state.dart';
|
||||||
|
|
||||||
import '../tenant/feature_gate.dart';
|
import '../tenant/feature_gate.dart';
|
||||||
|
|
||||||
/// الدراور — نقطة تطبيق **طبقتَي الميزات** معاً (docs/23 §5 + docs/38 §10):
|
/// الدراور — نقطة تطبيق **طبقتَي الميزات** معاً (docs/23 §5 + docs/38 §10):
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../design/tokens.dart';
|
import '../design/tokens.dart';
|
||||||
|
|
||||||
import '../design/tripz_colors.dart';
|
import '../design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
|
|
||||||
import 'tripz_button.dart';
|
import 'tripz_button.dart';
|
||||||
|
|
||||||
/// حالة الفراغ: رسالة **تدلّ على الفعل التالي** لا مجرّد «لا يوجد»
|
/// حالة الفراغ: رسالة **تدلّ على الفعل التالي** لا مجرّد «لا يوجد»
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../design/tokens.dart';
|
import '../design/tokens.dart';
|
||||||
|
|
||||||
import '../design/tripz_colors.dart';
|
import '../design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
|
|
||||||
import 'tripz_button.dart';
|
import 'tripz_button.dart';
|
||||||
|
|
||||||
/// حالة الخطأ: رسالة **بالعربية** مع «أعد المحاولة» — لا شاشة صامتة
|
/// حالة الخطأ: رسالة **بالعربية** مع «أعد المحاولة» — لا شاشة صامتة
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../features/auth/cubit/phone_error.dart';
|
import '../../features/auth/cubit/phone_error.dart';
|
||||||
|
|
||||||
import '../api/api_failure.dart';
|
import '../api/api_failure.dart';
|
||||||
|
|
||||||
import '../design/tokens.dart';
|
import '../design/tokens.dart';
|
||||||
|
|
||||||
import '../design/tripz_colors.dart';
|
import '../design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
|
|
||||||
/// النقطة **الوحيدة** التي تُترجَم فيها رموز الفشل إلى نصّ.
|
/// النقطة **الوحيدة** التي تُترجَم فيها رموز الفشل إلى نصّ.
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../design/tokens.dart';
|
import '../design/tokens.dart';
|
||||||
|
|
||||||
import '../design/tripz_colors.dart';
|
import '../design/tripz_colors.dart';
|
||||||
|
|
||||||
/// ترويسة موحّدة لشاشات الدخول: أيقونة ثم عنوان ثم سطر شارح.
|
/// ترويسة موحّدة لشاشات الدخول: أيقونة ثم عنوان ثم سطر شارح.
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import '../design/tokens.dart';
|
||||||
|
import '../design/tripz_colors.dart';
|
||||||
|
import '../design/typography.dart';
|
||||||
|
import '../payments/country_codes.dart';
|
||||||
|
|
||||||
|
/// حقل الهاتف بمفتاح دولة — النمط المجرَّب في سيرو.
|
||||||
|
///
|
||||||
|
/// المفتاح يُختار من ورقة سفلية لا من قائمة منسدلة: الأصابع على الهاتف
|
||||||
|
/// أدقّ مع أهداف كبيرة، والقائمة أربع دول لا مئتان.
|
||||||
|
///
|
||||||
|
/// يُخرج الرقم **كاملاً بالمفتاح بلا `+`** (`962790000001`) — وهو ما يقبله
|
||||||
|
/// الخادم ويطبّعه (docs/38 §2).
|
||||||
|
class PhoneField extends StatelessWidget {
|
||||||
|
const PhoneField({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
required this.country,
|
||||||
|
required this.onCountryChanged,
|
||||||
|
this.label,
|
||||||
|
this.hint,
|
||||||
|
this.errorText,
|
||||||
|
this.enabled = true,
|
||||||
|
this.onSubmitted,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TextEditingController controller;
|
||||||
|
final CountryCode country;
|
||||||
|
final ValueChanged<CountryCode> onCountryChanged;
|
||||||
|
final String? label;
|
||||||
|
final String? hint;
|
||||||
|
final String? errorText;
|
||||||
|
final bool enabled;
|
||||||
|
final ValueChanged<String>? onSubmitted;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TextField(
|
||||||
|
controller: controller,
|
||||||
|
enabled: enabled,
|
||||||
|
autofocus: true,
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
onSubmitted: onSubmitted,
|
||||||
|
maxLength: country.nationalLength + 1,
|
||||||
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
|
autofillHints: const [AutofillHints.telephoneNumber],
|
||||||
|
style: numericStyle(context.texts.bodyLarge),
|
||||||
|
// الرقم يُقرأ يساراً-ليميناً حتى في واجهة عربية.
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: label,
|
||||||
|
hintText: hint,
|
||||||
|
errorText: errorText,
|
||||||
|
counterText: '',
|
||||||
|
prefixIcon: _DialPrefix(
|
||||||
|
country: country,
|
||||||
|
enabled: enabled,
|
||||||
|
onTap: () => _pickCountry(context),
|
||||||
|
),
|
||||||
|
prefixIconConstraints: const BoxConstraints(minWidth: 0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickCountry(BuildContext context) async {
|
||||||
|
final lang = Localizations.localeOf(context).languageCode;
|
||||||
|
final picked = await showModalBottomSheet<CountryCode>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
for (final c in CountryCode.values)
|
||||||
|
ListTile(
|
||||||
|
leading: Text(c.flag, style: const TextStyle(fontSize: 24)),
|
||||||
|
title: Text(c.nameFor(lang)),
|
||||||
|
trailing: Text(
|
||||||
|
'+${c.dial}',
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
style: numericStyle(context.texts.bodyMedium),
|
||||||
|
),
|
||||||
|
selected: c == country,
|
||||||
|
onTap: () => Navigator.of(context).pop(c),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (picked != null) onCountryChanged(picked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DialPrefix extends StatelessWidget {
|
||||||
|
const _DialPrefix({
|
||||||
|
required this.country,
|
||||||
|
required this.enabled,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final CountryCode country;
|
||||||
|
final bool enabled;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: enabled ? onTap : null,
|
||||||
|
borderRadius: Radii.field_,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: Space.sm),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
children: [
|
||||||
|
Text(country.flag, style: const TextStyle(fontSize: 20)),
|
||||||
|
const SizedBox(width: Space.xxs),
|
||||||
|
Text(
|
||||||
|
'+${country.dial}',
|
||||||
|
style: numericStyle(context.texts.bodyLarge),
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.arrow_drop_down_rounded,
|
||||||
|
color: context.colors.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
const SizedBox(width: Space.xxs),
|
||||||
|
SizedBox(
|
||||||
|
height: 24,
|
||||||
|
child: VerticalDivider(
|
||||||
|
width: 1,
|
||||||
|
color: context.colors.outlineVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../design/tokens.dart';
|
import '../design/tokens.dart';
|
||||||
|
|
||||||
import '../design/tripz_colors.dart';
|
import '../design/tripz_colors.dart';
|
||||||
|
|
||||||
/// هيكل تحميل — **لا spinner فارغ** (docs/26 §8.4).
|
/// هيكل تحميل — **لا spinner فارغ** (docs/26 §8.4).
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../design/tokens.dart';
|
import '../design/tokens.dart';
|
||||||
|
|
||||||
import '../design/tripz_colors.dart';
|
import '../design/tripz_colors.dart';
|
||||||
|
|
||||||
enum BannerTone { info, success, warning, danger }
|
enum BannerTone { info, success, warning, danger }
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import '../design/tokens.dart';
|
import '../design/tokens.dart';
|
||||||
|
|
||||||
import '../design/typography.dart';
|
import '../design/typography.dart';
|
||||||
|
|
||||||
/// حقل رمز التحقّق — أربع خانات (طول رمز تريبز، docs/38 §2).
|
/// حقل رمز التحقّق — أربع خانات (طول رمز تريبز، docs/38 §2).
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../design/tokens.dart';
|
import '../design/tokens.dart';
|
||||||
|
|
||||||
import 'empty_view.dart';
|
import 'empty_view.dart';
|
||||||
|
|
||||||
import 'error_view.dart';
|
import 'error_view.dart';
|
||||||
|
|
||||||
import 'skeleton.dart';
|
import 'skeleton.dart';
|
||||||
|
|
||||||
import 'view_status.dart';
|
import 'view_status.dart';
|
||||||
|
|
||||||
/// نقطة التنظيم المركزية (docs/26 §6). **ممنوع `Scaffold` خام في
|
/// نقطة التنظيم المركزية (docs/26 §6). **ممنوع `Scaffold` خام في
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import '../design/tokens.dart';
|
import '../design/tokens.dart';
|
||||||
|
|
||||||
import '../design/typography.dart';
|
import '../design/typography.dart';
|
||||||
|
|
||||||
/// حقل الإدخال الموحّد (docs/26 §7): نصّ · هاتف · بحث.
|
/// حقل الإدخال الموحّد (docs/26 §7): نصّ · هاتف · بحث.
|
||||||
|
|||||||
@@ -1,22 +1,16 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_exception.dart';
|
import '../../../core/api/api_exception.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_failure.dart';
|
import '../../../core/api/api_failure.dart';
|
||||||
|
|
||||||
import '../../../core/config.dart';
|
import '../../../core/config.dart';
|
||||||
|
import '../../../core/payments/country_codes.dart';
|
||||||
import '../data/auth_repository.dart';
|
import '../data/auth_repository.dart';
|
||||||
|
|
||||||
import 'login_state.dart';
|
import 'login_state.dart';
|
||||||
|
|
||||||
import 'phone_error.dart';
|
import 'phone_error.dart';
|
||||||
|
|
||||||
/// تدفّق الدخول كاملاً: الشروط ← إذن الموقع ← الهاتف ← الرمز.
|
/// تدفّق الدخول كاملاً: الشروط ← إذن الموقع ← الهاتف ← الرمز.
|
||||||
@@ -121,6 +115,15 @@ class LoginCubit extends Cubit<LoginState> {
|
|||||||
|
|
||||||
// ── الهاتف ─────────────────────────────────────────────────────────────
|
// ── الهاتف ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// يُنادى عند الإقلاع بحزمة دولة المستأجر — فيرى الأردنيُّ الأردن أولاً.
|
||||||
|
void applyTenantCountry(String countryPack) {
|
||||||
|
if (state.phone.isNotEmpty) return; // لا نغيّر اختياراً واعياً للمستخدم
|
||||||
|
emit(state.copyWith(country: CountryCode.fromPack(countryPack)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void countryChanged(CountryCode country) =>
|
||||||
|
emit(state.copyWith(country: country, clearPhoneError: true));
|
||||||
|
|
||||||
void phoneChanged(String value) {
|
void phoneChanged(String value) {
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
phone: value,
|
phone: value,
|
||||||
@@ -130,14 +133,14 @@ class LoginCubit extends Cubit<LoginState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> submitPhone() async {
|
Future<void> submitPhone() async {
|
||||||
final error = validatePhone(state.phone);
|
final error = validatePhone(state.phone, state.country);
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
emit(state.copyWith(phoneError: error));
|
emit(state.copyWith(phoneError: error));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
emit(state.copyWith(status: LoginStatus.submitting, clearFailure: true));
|
emit(state.copyWith(status: LoginStatus.submitting, clearFailure: true));
|
||||||
try {
|
try {
|
||||||
await _repo.sendOtp(state.phone.trim());
|
await _repo.sendOtp(state.e164);
|
||||||
emit(state.copyWith(status: LoginStatus.idle, step: LoginStep.otp));
|
emit(state.copyWith(status: LoginStatus.idle, step: LoginStep.otp));
|
||||||
_startResendCountdown();
|
_startResendCountdown();
|
||||||
} on ApiException catch (e) {
|
} on ApiException catch (e) {
|
||||||
@@ -164,7 +167,7 @@ class LoginCubit extends Cubit<LoginState> {
|
|||||||
if (!state.canResend) return;
|
if (!state.canResend) return;
|
||||||
emit(state.copyWith(status: LoginStatus.submitting, clearFailure: true));
|
emit(state.copyWith(status: LoginStatus.submitting, clearFailure: true));
|
||||||
try {
|
try {
|
||||||
await _repo.sendOtp(state.phone.trim());
|
await _repo.sendOtp(state.e164);
|
||||||
emit(state.copyWith(status: LoginStatus.idle));
|
emit(state.copyWith(status: LoginStatus.idle));
|
||||||
_startResendCountdown();
|
_startResendCountdown();
|
||||||
} on ApiException catch (e) {
|
} on ApiException catch (e) {
|
||||||
@@ -182,7 +185,7 @@ class LoginCubit extends Cubit<LoginState> {
|
|||||||
if (code.length != AppConfig.otpLength) return;
|
if (code.length != AppConfig.otpLength) return;
|
||||||
emit(state.copyWith(status: LoginStatus.submitting, clearFailure: true));
|
emit(state.copyWith(status: LoginStatus.submitting, clearFailure: true));
|
||||||
try {
|
try {
|
||||||
final user = await _repo.verifyOtp(phone: state.phone.trim(), code: code);
|
final user = await _repo.verifyOtp(phone: state.e164, code: code);
|
||||||
_resendTimer?.cancel();
|
_resendTimer?.cancel();
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
status: LoginStatus.success,
|
status: LoginStatus.success,
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_failure.dart';
|
import '../../../core/api/api_failure.dart';
|
||||||
|
import '../../../core/payments/country_codes.dart';
|
||||||
import '../../../core/session/app_user.dart';
|
import '../../../core/session/app_user.dart';
|
||||||
|
|
||||||
import 'phone_error.dart';
|
import 'phone_error.dart';
|
||||||
|
|
||||||
/// خطوات الدخول بالترتيب الميداني المثبت في سيرو (docs/39 §2): بوابتان
|
/// خطوات الدخول بالترتيب الميداني المثبت في سيرو (docs/39 §2): بوابتان
|
||||||
@@ -17,6 +16,7 @@ class LoginState extends Equatable {
|
|||||||
this.step = LoginStep.agreement,
|
this.step = LoginStep.agreement,
|
||||||
this.status = LoginStatus.idle,
|
this.status = LoginStatus.idle,
|
||||||
this.phone = '',
|
this.phone = '',
|
||||||
|
this.country = CountryCode.jordan,
|
||||||
this.agreed = false,
|
this.agreed = false,
|
||||||
this.failure,
|
this.failure,
|
||||||
this.phoneError,
|
this.phoneError,
|
||||||
@@ -31,6 +31,13 @@ class LoginState extends Equatable {
|
|||||||
/// الرقم كما أدخله المستخدم — الخادم يطبّعه، فلا نطبّعه نحن (docs/38 §2).
|
/// الرقم كما أدخله المستخدم — الخادم يطبّعه، فلا نطبّعه نحن (docs/38 §2).
|
||||||
final String phone;
|
final String phone;
|
||||||
|
|
||||||
|
/// الدولة المختارة — تحدّد المفتاح الدولي وطول التحقّق. تُبدأ من
|
||||||
|
/// `countryPack` المستأجر لا من لغة الجهاز.
|
||||||
|
final CountryCode country;
|
||||||
|
|
||||||
|
/// ما يُرسل للخادم: المفتاح + الرقم بلا `+` (`962790000001`).
|
||||||
|
String get e164 => '${country.dial}${phone.trim()}';
|
||||||
|
|
||||||
final bool agreed;
|
final bool agreed;
|
||||||
final ApiFailure? failure;
|
final ApiFailure? failure;
|
||||||
final PhoneError? phoneError;
|
final PhoneError? phoneError;
|
||||||
@@ -51,6 +58,7 @@ class LoginState extends Equatable {
|
|||||||
LoginStep? step,
|
LoginStep? step,
|
||||||
LoginStatus? status,
|
LoginStatus? status,
|
||||||
String? phone,
|
String? phone,
|
||||||
|
CountryCode? country,
|
||||||
bool? agreed,
|
bool? agreed,
|
||||||
ApiFailure? failure,
|
ApiFailure? failure,
|
||||||
PhoneError? phoneError,
|
PhoneError? phoneError,
|
||||||
@@ -64,6 +72,7 @@ class LoginState extends Equatable {
|
|||||||
step: step ?? this.step,
|
step: step ?? this.step,
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
phone: phone ?? this.phone,
|
phone: phone ?? this.phone,
|
||||||
|
country: country ?? this.country,
|
||||||
agreed: agreed ?? this.agreed,
|
agreed: agreed ?? this.agreed,
|
||||||
failure: clearFailure ? null : (failure ?? this.failure),
|
failure: clearFailure ? null : (failure ?? this.failure),
|
||||||
phoneError: clearPhoneError ? null : (phoneError ?? this.phoneError),
|
phoneError: clearPhoneError ? null : (phoneError ?? this.phoneError),
|
||||||
@@ -79,6 +88,7 @@ class LoginState extends Equatable {
|
|||||||
step,
|
step,
|
||||||
status,
|
status,
|
||||||
phone,
|
phone,
|
||||||
|
country,
|
||||||
agreed,
|
agreed,
|
||||||
failure,
|
failure,
|
||||||
phoneError,
|
phoneError,
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
|
import '../../../core/payments/country_codes.dart';
|
||||||
|
|
||||||
/// أخطاء تحقّق **محليّة** لرقم الهاتف — تُكتشف قبل أي نداء شبكة.
|
/// أخطاء تحقّق **محليّة** لرقم الهاتف — تُكتشف قبل أي نداء شبكة.
|
||||||
/// الشروط الثلاثة منقولة من التحقّق المطبَّق ميدانياً في سيرو (docs/39 §2).
|
/// الشروط الثلاثة منقولة من التحقّق المطبَّق ميدانياً في سيرو (docs/39 §2).
|
||||||
enum PhoneError { empty, leadingZero, tooShort }
|
enum PhoneError { empty, leadingZero, tooShort }
|
||||||
|
|
||||||
PhoneError? validatePhone(String raw) {
|
/// الطول المتوقّع من **الدولة المختارة** لا رقماً ثابتاً: الأردن وسوريا 9
|
||||||
|
/// خانات، ومصر والعراق 10.
|
||||||
|
PhoneError? validatePhone(String raw, CountryCode country) {
|
||||||
final v = raw.trim();
|
final v = raw.trim();
|
||||||
if (v.isEmpty) return PhoneError.empty;
|
if (v.isEmpty) return PhoneError.empty;
|
||||||
if (v.startsWith('0')) return PhoneError.leadingZero;
|
if (v.startsWith('0')) return PhoneError.leadingZero;
|
||||||
if (v.length < 9) return PhoneError.tooShort;
|
if (v.length < country.nationalLength) return PhoneError.tooShort;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import '../../../core/api/api_client.dart';
|
import '../../../core/api/api_client.dart';
|
||||||
|
|
||||||
import '../../../core/session/app_user.dart';
|
import '../../../core/session/app_user.dart';
|
||||||
|
|
||||||
import '../../../core/session/session_source.dart';
|
import '../../../core/session/session_source.dart';
|
||||||
|
|
||||||
import '../../../core/storage/token_store.dart';
|
import '../../../core/storage/token_store.dart';
|
||||||
|
|
||||||
/// كل نداءات المصادقة (docs/38 §2). الـCubit لا ينادي Dio مباشرة أبداً
|
/// كل نداءات المصادقة (docs/38 §2). الـCubit لا ينادي Dio مباشرة أبداً
|
||||||
|
|||||||
@@ -1,22 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/design/tokens.dart';
|
import '../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../core/design/tripz_colors.dart';
|
import '../../../core/design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../../../core/l10n/l10n.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 '../../../core/ui/form_header.dart';
|
import '../../../core/ui/form_header.dart';
|
||||||
|
import '../../../core/ui/tripz_button.dart';
|
||||||
|
import '../../../core/ui/tripz_scaffold.dart';
|
||||||
|
import '../cubit/login_cubit.dart';
|
||||||
|
import '../cubit/login_state.dart';
|
||||||
|
|
||||||
/// البوابة الأولى: شاشة كاملة لا حوار، وزر متابعة **معطّل حتى يُؤشَّر**
|
/// البوابة الأولى: شاشة كاملة لا حوار، وزر متابعة **معطّل حتى يُؤشَّر**
|
||||||
/// (docs/39 §2). تُحفظ الموافقة فلا تتكرر.
|
/// (docs/39 §2). تُحفظ الموافقة فلا تتكرر.
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/di.dart';
|
import '../../../core/di.dart';
|
||||||
|
|
||||||
import '../../../core/session/session_cubit.dart';
|
import '../../../core/session/session_cubit.dart';
|
||||||
|
|
||||||
import '../cubit/login_cubit.dart';
|
import '../cubit/login_cubit.dart';
|
||||||
|
|
||||||
import '../cubit/login_state.dart';
|
import '../cubit/login_state.dart';
|
||||||
|
|
||||||
import 'agreement_view.dart';
|
import 'agreement_view.dart';
|
||||||
|
|
||||||
import 'otp_view.dart';
|
import 'otp_view.dart';
|
||||||
|
|
||||||
import 'permission_view.dart';
|
import 'permission_view.dart';
|
||||||
|
|
||||||
import 'phone_view.dart';
|
import 'phone_view.dart';
|
||||||
|
|
||||||
/// مضيف تدفّق الدخول: خطوة واحدة معروضة في كل لحظة، والانتقال بينها من
|
/// مضيف تدفّق الدخول: خطوة واحدة معروضة في كل لحظة، والانتقال بينها من
|
||||||
|
|||||||
@@ -1,26 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/design/tokens.dart';
|
import '../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../core/design/tripz_colors.dart';
|
import '../../../core/design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../../../core/l10n/l10n.dart';
|
import '../../../core/l10n/l10n.dart';
|
||||||
|
|
||||||
import '../../../core/ui/failure_text.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 '../../../core/ui/form_header.dart';
|
import '../../../core/ui/form_header.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';
|
||||||
|
|
||||||
class OtpView extends StatefulWidget {
|
class OtpView extends StatefulWidget {
|
||||||
const OtpView({super.key});
|
const OtpView({super.key});
|
||||||
|
|||||||
@@ -1,22 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/design/tokens.dart';
|
import '../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../core/l10n/l10n.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 '../../../core/ui/form_header.dart';
|
import '../../../core/ui/form_header.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';
|
||||||
|
|
||||||
/// البوابة الثانية: إذن الموقع. عند الرفض النهائي لا يبقى إلا فتح إعدادات
|
/// البوابة الثانية: إذن الموقع. عند الرفض النهائي لا يبقى إلا فتح إعدادات
|
||||||
/// النظام — والعودة منها يلتقطها `LoginPage` بمراقبة دورة الحياة.
|
/// النظام — والعودة منها يلتقطها `LoginPage` بمراقبة دورة الحياة.
|
||||||
|
|||||||
@@ -1,28 +1,48 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/design/tokens.dart';
|
import '../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../core/l10n/l10n.dart';
|
import '../../../core/l10n/l10n.dart';
|
||||||
|
import '../../../core/tenant/tenant_cubit.dart';
|
||||||
import '../../../core/ui/failure_text.dart';
|
import '../../../core/ui/failure_text.dart';
|
||||||
|
import '../../../core/ui/form_header.dart';
|
||||||
|
import '../../../core/ui/phone_field.dart';
|
||||||
import '../../../core/ui/tripz_button.dart';
|
import '../../../core/ui/tripz_button.dart';
|
||||||
|
|
||||||
import '../../../core/ui/tripz_scaffold.dart';
|
import '../../../core/ui/tripz_scaffold.dart';
|
||||||
|
|
||||||
import '../../../core/ui/tripz_text_field.dart';
|
|
||||||
|
|
||||||
import '../cubit/login_cubit.dart';
|
import '../cubit/login_cubit.dart';
|
||||||
|
|
||||||
import '../cubit/login_state.dart';
|
import '../cubit/login_state.dart';
|
||||||
|
|
||||||
import '../../../core/ui/form_header.dart';
|
class PhoneView extends StatefulWidget {
|
||||||
|
|
||||||
class PhoneView extends StatelessWidget {
|
|
||||||
const PhoneView({super.key});
|
const PhoneView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PhoneView> createState() => _PhoneViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PhoneViewState extends State<PhoneView> {
|
||||||
|
final _phone = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_phone.addListener(
|
||||||
|
() => context.read<LoginCubit>().phoneChanged(_phone.text),
|
||||||
|
);
|
||||||
|
// الدولة الافتراضية من المستأجر — يُطبَّق بعد أول إطار كي يتوفّر السياق.
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context
|
||||||
|
.read<LoginCubit>()
|
||||||
|
.applyTenantCountry(context.read<TenantCubit>().state.countryPack);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_phone.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
@@ -30,6 +50,7 @@ class PhoneView extends StatelessWidget {
|
|||||||
buildWhen: (p, n) =>
|
buildWhen: (p, n) =>
|
||||||
p.status != n.status ||
|
p.status != n.status ||
|
||||||
p.phoneError != n.phoneError ||
|
p.phoneError != n.phoneError ||
|
||||||
|
p.country != n.country ||
|
||||||
p.failure != n.failure,
|
p.failure != n.failure,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final cubit = context.read<LoginCubit>();
|
final cubit = context.read<LoginCubit>();
|
||||||
@@ -49,11 +70,13 @@ class PhoneView extends StatelessWidget {
|
|||||||
lead: l10n.phoneLead,
|
lead: l10n.phoneLead,
|
||||||
),
|
),
|
||||||
const SizedBox(height: Space.lg),
|
const SizedBox(height: Space.lg),
|
||||||
TripzTextField.phone(
|
PhoneField(
|
||||||
|
controller: _phone,
|
||||||
|
country: state.country,
|
||||||
|
onCountryChanged: cubit.countryChanged,
|
||||||
label: l10n.phoneLabel,
|
label: l10n.phoneLabel,
|
||||||
hint: l10n.phoneHint,
|
hint: l10n.phoneHint,
|
||||||
enabled: !state.isSubmitting,
|
enabled: !state.isSubmitting,
|
||||||
onChanged: cubit.phoneChanged,
|
|
||||||
onSubmitted: (_) => cubit.submitPhone(),
|
onSubmitted: (_) => cubit.submitPhone(),
|
||||||
errorText: state.phoneError == null
|
errorText: state.phoneError == null
|
||||||
? null
|
? null
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_exception.dart';
|
import '../../../core/api/api_exception.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_failure.dart';
|
import '../../../core/api/api_failure.dart';
|
||||||
|
|
||||||
import '../../../core/session/app_user.dart';
|
import '../../../core/session/app_user.dart';
|
||||||
|
|
||||||
import '../../../core/ui/view_status.dart';
|
import '../../../core/ui/view_status.dart';
|
||||||
|
|
||||||
import '../data/profile_repository.dart';
|
import '../data/profile_repository.dart';
|
||||||
|
|
||||||
class ProfileState {
|
class ProfileState {
|
||||||
|
|||||||
@@ -1,23 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/design/tokens.dart';
|
import '../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../core/design/tripz_colors.dart';
|
import '../../../core/design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../../../core/di.dart';
|
import '../../../core/di.dart';
|
||||||
|
|
||||||
import '../../../core/l10n/l10n.dart';
|
import '../../../core/l10n/l10n.dart';
|
||||||
|
|
||||||
import '../../../core/session/session_cubit.dart';
|
import '../../../core/session/session_cubit.dart';
|
||||||
|
|
||||||
import '../../../core/ui/tripz_button.dart';
|
import '../../../core/ui/tripz_button.dart';
|
||||||
|
|
||||||
import '../../../core/ui/tripz_scaffold.dart';
|
import '../../../core/ui/tripz_scaffold.dart';
|
||||||
|
|
||||||
import '../../../core/ui/tripz_text_field.dart';
|
import '../../../core/ui/tripz_text_field.dart';
|
||||||
|
|
||||||
import '../cubit/profile_cubit.dart';
|
import '../cubit/profile_cubit.dart';
|
||||||
|
|
||||||
/// تعديل الاسم. **الرقم لا يُعدَّل**: هو هوية الحساب على الخادم، وتغييره
|
/// تعديل الاسم. **الرقم لا يُعدَّل**: هو هوية الحساب على الخادم، وتغييره
|
||||||
|
|||||||
@@ -1,25 +1,15 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/design/tokens.dart';
|
import '../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../core/di.dart';
|
import '../../../core/di.dart';
|
||||||
|
|
||||||
import '../../../core/l10n/l10n.dart';
|
import '../../../core/l10n/l10n.dart';
|
||||||
|
|
||||||
import '../../../core/session/session_cubit.dart';
|
import '../../../core/session/session_cubit.dart';
|
||||||
|
|
||||||
import '../../../core/ui/failure_text.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 '../../../core/ui/form_header.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 '../cubit/profile_cubit.dart';
|
||||||
|
|
||||||
class ProfileSetupPage extends StatefulWidget {
|
class ProfileSetupPage extends StatefulWidget {
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_exception.dart';
|
import '../../../core/api/api_exception.dart';
|
||||||
|
|
||||||
import '../data/rating_repository.dart';
|
import '../data/rating_repository.dart';
|
||||||
|
|
||||||
import 'rating_state.dart';
|
import 'rating_state.dart';
|
||||||
|
|
||||||
class RatingCubit extends Cubit<RatingState> {
|
class RatingCubit extends Cubit<RatingState> {
|
||||||
|
|||||||
@@ -1,27 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/design/tokens.dart';
|
import '../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../core/design/tripz_colors.dart';
|
import '../../../core/design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../../../core/di.dart';
|
import '../../../core/di.dart';
|
||||||
|
|
||||||
import '../../../core/l10n/l10n.dart';
|
import '../../../core/l10n/l10n.dart';
|
||||||
|
|
||||||
import '../../../core/ui/tripz_button.dart';
|
import '../../../core/ui/tripz_button.dart';
|
||||||
|
|
||||||
import '../../../core/ui/tripz_scaffold.dart';
|
import '../../../core/ui/tripz_scaffold.dart';
|
||||||
|
|
||||||
import '../../../core/ui/tripz_text_field.dart';
|
import '../../../core/ui/tripz_text_field.dart';
|
||||||
|
|
||||||
import '../cubit/rating_cubit.dart';
|
import '../cubit/rating_cubit.dart';
|
||||||
|
|
||||||
import '../cubit/rating_state.dart';
|
import '../cubit/rating_state.dart';
|
||||||
|
|
||||||
import '../data/rating_repository.dart';
|
import '../data/rating_repository.dart';
|
||||||
|
|
||||||
import 'star_row.dart';
|
import 'star_row.dart';
|
||||||
|
|
||||||
/// شاشة التقييم — **تُفرض عند فتح التطبيق** إن كانت هناك رحلة معلّقة
|
/// شاشة التقييم — **تُفرض عند فتح التطبيق** إن كانت هناك رحلة معلّقة
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../../core/design/tokens.dart';
|
import '../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../core/design/tripz_colors.dart';
|
import '../../../core/design/tripz_colors.dart';
|
||||||
|
|
||||||
/// خمس نجوم — أهداف لمس ≥ 48 لأن الشاشة تُستعمل عادةً والمستخدم مستعجل.
|
/// خمس نجوم — أهداف لمس ≥ 48 لأن الشاشة تُستعمل عادةً والمستخدم مستعجل.
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import 'settings_state.dart';
|
import 'settings_state.dart';
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// المظهر واللغة (docs/26 §4) — محفوظان محلياً، وليسا سرّاً فيذهبان إلى
|
/// المظهر واللغة (docs/26 §4) — محفوظان محلياً، وليسا سرّاً فيذهبان إلى
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/design/tokens.dart';
|
import '../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../core/design/tripz_colors.dart';
|
import '../../../core/design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../../../core/l10n/l10n.dart';
|
import '../../../core/l10n/l10n.dart';
|
||||||
|
|
||||||
import '../../../core/ui/tripz_scaffold.dart';
|
import '../../../core/ui/tripz_scaffold.dart';
|
||||||
|
|
||||||
import '../cubit/settings_cubit.dart';
|
import '../cubit/settings_cubit.dart';
|
||||||
|
|
||||||
import '../cubit/settings_state.dart';
|
import '../cubit/settings_state.dart';
|
||||||
|
|
||||||
/// المظهر واللغة. تبديل اللغة **يقلب الاتجاه كاملاً** بلا إعادة تشغيل —
|
/// المظهر واللغة. تبديل اللغة **يقلب الاتجاه كاملاً** بلا إعادة تشغيل —
|
||||||
|
|||||||
@@ -1,19 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/build_config.dart';
|
import '../../../core/build_config.dart';
|
||||||
|
|
||||||
import '../../../core/config.dart';
|
import '../../../core/config.dart';
|
||||||
|
|
||||||
import '../../../core/design/tokens.dart';
|
import '../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../core/design/tripz_colors.dart';
|
import '../../../core/design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../../../core/l10n/l10n.dart';
|
import '../../../core/l10n/l10n.dart';
|
||||||
|
|
||||||
import '../../../core/session/session_cubit.dart';
|
import '../../../core/session/session_cubit.dart';
|
||||||
|
|
||||||
import '../../../core/tenant/tenant_cubit.dart';
|
import '../../../core/tenant/tenant_cubit.dart';
|
||||||
|
|
||||||
/// شاشة الإقلاع: تستعيد الجلسة ثم يتولّى الراوتر التوجيه. لا تنقّل من هنا —
|
/// شاشة الإقلاع: تستعيد الجلسة ثم يتولّى الراوتر التوجيه. لا تنقّل من هنا —
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../../core/build_config.dart';
|
import '../../../core/build_config.dart';
|
||||||
|
|
||||||
import '../../../core/design/tokens.dart';
|
import '../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../core/design/tripz_colors.dart';
|
import '../../../core/design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../../../core/l10n/l10n.dart';
|
import '../../../core/l10n/l10n.dart';
|
||||||
|
|
||||||
import '../../../core/ui/tripz_card.dart';
|
import '../../../core/ui/tripz_card.dart';
|
||||||
|
|
||||||
import '../../../core/ui/tripz_scaffold.dart';
|
import '../../../core/ui/tripz_scaffold.dart';
|
||||||
|
|
||||||
/// المساعدة و«عن التطبيق». محتوى محلّي عمداً: الخادم لا يوفّر نقطة أسئلة
|
/// المساعدة و«عن التطبيق». محتوى محلّي عمداً: الخادم لا يوفّر نقطة أسئلة
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_exception.dart';
|
import '../../../core/api/api_exception.dart';
|
||||||
|
|
||||||
import '../../../core/ui/view_status.dart';
|
import '../../../core/ui/view_status.dart';
|
||||||
|
|
||||||
import '../data/history_repository.dart';
|
import '../data/history_repository.dart';
|
||||||
|
|
||||||
import '../data/models/trip.dart';
|
import '../data/models/trip.dart';
|
||||||
|
|
||||||
class HistoryState {
|
class HistoryState {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import '../../../core/api/api_client.dart';
|
import '../../../core/api/api_client.dart';
|
||||||
|
|
||||||
import 'models/trip.dart';
|
import 'models/trip.dart';
|
||||||
|
|
||||||
/// سجلّ الرحلات. لا يستورد ميزة الرحلة كمنطق — يعيد استعمال نموذجها فقط،
|
/// سجلّ الرحلات. لا يستورد ميزة الرحلة كمنطق — يعيد استعمال نموذجها فقط،
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import '../../../core/api/antlaq_api.dart';
|
import '../../../core/api/antlaq_api.dart';
|
||||||
|
|
||||||
import 'models/geo_point.dart';
|
import 'models/geo_point.dart';
|
||||||
|
|
||||||
import 'models/place.dart';
|
import 'models/place.dart';
|
||||||
|
|
||||||
import 'models/route_info.dart';
|
import 'models/route_info.dart';
|
||||||
|
|
||||||
/// كل الخرائط من map-saas مباشرة (قرار 2026-07-20). نقاط `/maps/*` في باك إند
|
/// كل الخرائط من map-saas مباشرة (قرار 2026-07-20). نقاط `/maps/*` في باك إند
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import '../../../core/api/api_client.dart';
|
import '../../../core/api/api_client.dart';
|
||||||
|
|
||||||
import 'models/fare_quote.dart';
|
import 'models/fare_quote.dart';
|
||||||
|
|
||||||
import 'models/geo_point.dart';
|
import 'models/geo_point.dart';
|
||||||
|
|
||||||
import 'models/ride_type.dart';
|
import 'models/ride_type.dart';
|
||||||
|
|
||||||
import 'models/trip.dart';
|
import 'models/trip.dart';
|
||||||
|
|
||||||
class TripRepository {
|
class TripRepository {
|
||||||
|
|||||||
@@ -1,27 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/design/tokens.dart';
|
import '../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../core/design/tripz_colors.dart';
|
import '../../../core/design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../../../core/design/typography.dart';
|
import '../../../core/design/typography.dart';
|
||||||
|
|
||||||
import '../../../core/di.dart';
|
import '../../../core/di.dart';
|
||||||
|
|
||||||
import '../../../core/format/money.dart';
|
import '../../../core/format/money.dart';
|
||||||
|
|
||||||
import '../../../core/l10n/l10n.dart';
|
import '../../../core/l10n/l10n.dart';
|
||||||
|
|
||||||
import '../../../core/ui/empty_view.dart';
|
import '../../../core/ui/empty_view.dart';
|
||||||
|
|
||||||
import '../../../core/ui/tripz_card.dart';
|
import '../../../core/ui/tripz_card.dart';
|
||||||
|
|
||||||
import '../../../core/ui/tripz_scaffold.dart';
|
import '../../../core/ui/tripz_scaffold.dart';
|
||||||
|
|
||||||
import '../cubit/history_cubit.dart';
|
import '../cubit/history_cubit.dart';
|
||||||
|
|
||||||
import '../data/models/trip.dart';
|
import '../data/models/trip.dart';
|
||||||
|
|
||||||
class HistoryPage extends StatefulWidget {
|
class HistoryPage extends StatefulWidget {
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../../../core/design/tokens.dart';
|
import '../../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../../core/design/tripz_colors.dart';
|
import '../../../../core/design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../../../../core/design/typography.dart';
|
import '../../../../core/design/typography.dart';
|
||||||
|
|
||||||
import '../../data/models/trip.dart';
|
import '../../data/models/trip.dart';
|
||||||
|
|
||||||
/// بطاقة السائق ولوحة المركبة — أهم ما يبحث عنه الراكب في الشارع، فيأخذ
|
/// بطاقة السائق ولوحة المركبة — أهم ما يبحث عنه الراكب في الشارع، فيأخذ
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../../../core/design/tokens.dart';
|
import '../../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../../core/design/tripz_colors.dart';
|
import '../../../../core/design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../../../../core/design/typography.dart';
|
import '../../../../core/design/typography.dart';
|
||||||
|
|
||||||
import '../../../../core/format/money.dart';
|
import '../../../../core/format/money.dart';
|
||||||
|
|
||||||
import '../../../../core/l10n/l10n.dart';
|
import '../../../../core/l10n/l10n.dart';
|
||||||
|
|
||||||
import '../../data/models/trip.dart';
|
import '../../data/models/trip.dart';
|
||||||
|
|
||||||
/// ملخّص الأجرة عند الإنهاء.
|
/// ملخّص الأجرة عند الإنهاء.
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_exception.dart';
|
import '../../../core/api/api_exception.dart';
|
||||||
|
|
||||||
import '../../../core/ui/view_status.dart';
|
import '../../../core/ui/view_status.dart';
|
||||||
|
|
||||||
import '../data/wallet_repository.dart';
|
import '../data/wallet_repository.dart';
|
||||||
|
|
||||||
import 'wallet_state.dart';
|
import 'wallet_state.dart';
|
||||||
|
|
||||||
class WalletCubit extends Cubit<WalletState> {
|
class WalletCubit extends Cubit<WalletState> {
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
import '../../../core/ui/view_status.dart';
|
import '../../../core/ui/view_status.dart';
|
||||||
|
|
||||||
import '../data/models/wallet.dart';
|
import '../data/models/wallet.dart';
|
||||||
|
|
||||||
import '../data/models/wallet_txn.dart';
|
import '../data/models/wallet_txn.dart';
|
||||||
|
|
||||||
enum WalletAction { none, toppingUp, topupFailed, topupDone }
|
enum WalletAction { none, toppingUp, topupFailed, topupDone }
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import '../../../core/api/api_client.dart';
|
import '../../../core/api/api_client.dart';
|
||||||
|
|
||||||
import 'models/wallet.dart';
|
import 'models/wallet.dart';
|
||||||
|
|
||||||
import 'models/wallet_txn.dart';
|
import 'models/wallet_txn.dart';
|
||||||
|
|
||||||
class WalletRepository {
|
class WalletRepository {
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../../../core/design/tokens.dart';
|
import '../../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../../core/design/tripz_colors.dart';
|
import '../../../../core/design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../../../../core/design/typography.dart';
|
import '../../../../core/design/typography.dart';
|
||||||
|
|
||||||
import '../../../../core/format/money.dart';
|
import '../../../../core/format/money.dart';
|
||||||
|
|
||||||
/// بطاقة الرصيد — الرقم هو بطل الشاشة، فيأخذ أكبر حجم وأعلى تباين.
|
/// بطاقة الرصيد — الرقم هو بطل الشاشة، فيأخذ أكبر حجم وأعلى تباين.
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../../../core/design/tokens.dart';
|
import '../../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../../core/design/tripz_colors.dart';
|
import '../../../../core/design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../../../../core/design/typography.dart';
|
import '../../../../core/design/typography.dart';
|
||||||
|
|
||||||
import '../../../../core/format/money.dart';
|
import '../../../../core/format/money.dart';
|
||||||
|
|
||||||
import '../../data/models/wallet_txn.dart';
|
import '../../data/models/wallet_txn.dart';
|
||||||
|
|
||||||
class TxnTile extends StatelessWidget {
|
class TxnTile extends StatelessWidget {
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import 'app.dart';
|
import 'app.dart';
|
||||||
|
|
||||||
import 'core/di.dart';
|
import 'core/di.dart';
|
||||||
|
|
||||||
import 'core/session/session_cubit.dart';
|
import 'core/session/session_cubit.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ cd "$(dirname "$0")/.."
|
|||||||
# ملفات يملكها السائق ولا تُنسخ من الراكب أبداً
|
# ملفات يملكها السائق ولا تُنسخ من الراكب أبداً
|
||||||
OWN=(
|
OWN=(
|
||||||
"core/config.dart" # appRole = driver
|
"core/config.dart" # appRole = driver
|
||||||
"core/build_config.dart" # اسم التطبيق
|
"core/build_config.dart" # اسم التطبيق (الألوان تُحدَّث يدوياً معاً)
|
||||||
"core/di.dart" # DutyCubit + OnboardingCubit + PayoutCubit
|
"core/di.dart" # DutyCubit + OnboardingCubit + PayoutCubit
|
||||||
"core/router.dart" # DutyPage + EarningsPage + بوابة التسجيل
|
"core/router.dart" # DutyPage + EarningsPage + بوابة التسجيل
|
||||||
"core/l10n/app_ar.arb"
|
"core/l10n/app_ar.arb"
|
||||||
|
|||||||
@@ -1,23 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
|
||||||
import 'core/build_config.dart';
|
import 'core/build_config.dart';
|
||||||
|
|
||||||
import 'core/design/theme.dart';
|
import 'core/design/theme.dart';
|
||||||
|
|
||||||
import 'core/di.dart';
|
import 'core/di.dart';
|
||||||
|
|
||||||
import 'core/l10n/l10n.dart';
|
import 'core/l10n/l10n.dart';
|
||||||
|
|
||||||
import 'core/router.dart';
|
import 'core/router.dart';
|
||||||
|
|
||||||
import 'core/tenant/tenant_cubit.dart';
|
import 'core/tenant/tenant_cubit.dart';
|
||||||
|
|
||||||
import 'features/settings/cubit/settings_cubit.dart';
|
import 'features/settings/cubit/settings_cubit.dart';
|
||||||
|
|
||||||
import 'features/settings/cubit/settings_state.dart';
|
import 'features/settings/cubit/settings_state.dart';
|
||||||
|
|
||||||
class TripzApp extends StatelessWidget {
|
class TripzApp extends StatelessWidget {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
import '../config.dart';
|
import '../config.dart';
|
||||||
|
|
||||||
import 'api_exception.dart';
|
import 'api_exception.dart';
|
||||||
|
|
||||||
/// عميل map-saas — **منفصل تماماً** عن `ApiClient`.
|
/// عميل map-saas — **منفصل تماماً** عن `ApiClient`.
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
import '../config.dart';
|
import '../config.dart';
|
||||||
|
|
||||||
import '../storage/token_store.dart';
|
import '../storage/token_store.dart';
|
||||||
|
|
||||||
import 'api_exception.dart';
|
import 'api_exception.dart';
|
||||||
|
|
||||||
import 'auth_interceptor.dart';
|
import 'auth_interceptor.dart';
|
||||||
|
|
||||||
/// الواجهة الوحيدة للخادم. لا تُنشأ `Dio` في أي مكان آخر.
|
/// الواجهة الوحيدة للخادم. لا تُنشأ `Dio` في أي مكان آخر.
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import 'dart:async';
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
import '../build_config.dart';
|
import '../build_config.dart';
|
||||||
|
|
||||||
import '../config.dart';
|
import '../config.dart';
|
||||||
|
|
||||||
import '../storage/token_store.dart';
|
import '../storage/token_store.dart';
|
||||||
|
|
||||||
/// يثبّت ترويسات كل طلب، ويجدّد التوكن **استباقياً** قبل انتهائه.
|
/// يثبّت ترويسات كل طلب، ويجدّد التوكن **استباقياً** قبل انتهائه.
|
||||||
|
|||||||
@@ -12,7 +12,13 @@ class BuildConfig {
|
|||||||
static const String tenantSlug = 'siro';
|
static const String tenantSlug = 'siro';
|
||||||
|
|
||||||
/// اللون البذرة — منه تُشتقّ `ColorScheme` للوضعين.
|
/// اللون البذرة — منه تُشتقّ `ColorScheme` للوضعين.
|
||||||
static const Color seedColor = Color(0xFF1F6FEB);
|
///
|
||||||
|
/// كحليّ سيرو (`#1A2340`) بقرار المالك 2026-08-05: الهوية البصرية تقترب من
|
||||||
|
/// سيرو المجرَّب ميدانياً بدل الأزرق العام.
|
||||||
|
static const Color seedColor = Color(0xFF1A2340);
|
||||||
|
|
||||||
|
/// اللون الثانوي — بنفسجي فاتح، يُستعمل للتمييز والحالات النشطة.
|
||||||
|
static const Color accentColor = Color(0xFF8C9CF8);
|
||||||
|
|
||||||
static const String logoAsset = 'assets/images/logo.png';
|
static const String logoAsset = 'assets/images/logo.png';
|
||||||
static const String splashLogoAsset = 'assets/images/splash_logo.png';
|
static const String splashLogoAsset = 'assets/images/splash_logo.png';
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../build_config.dart';
|
import '../build_config.dart';
|
||||||
|
|
||||||
import 'tokens.dart';
|
import 'tokens.dart';
|
||||||
|
|
||||||
import 'tripz_colors.dart';
|
import 'tripz_colors.dart';
|
||||||
|
|
||||||
import 'typography.dart';
|
import 'typography.dart';
|
||||||
|
|
||||||
/// دالة بناء الثيم الوحيدة (docs/26 §4): مدخلان لا ثالث لهما.
|
/// دالة بناء الثيم الوحيدة (docs/26 §4): مدخلان لا ثالث لهما.
|
||||||
@@ -13,9 +10,32 @@ import 'typography.dart';
|
|||||||
/// الاتجاه (RTL/LTR) **لا يُفرض هنا** — `MaterialApp` يشتقّه من الـlocale
|
/// الاتجاه (RTL/LTR) **لا يُفرض هنا** — `MaterialApp` يشتقّه من الـlocale
|
||||||
/// وحده. فرض `Directionality.rtl` يكسر الإنجليزية (docs/26 §4).
|
/// وحده. فرض `Directionality.rtl` يكسر الإنجليزية (docs/26 §4).
|
||||||
ThemeData buildTheme(Brightness brightness, Locale locale) {
|
ThemeData buildTheme(Brightness brightness, Locale locale) {
|
||||||
|
// البذرة الكحلية داكنة جداً، و`fromSeed` وحدها تنتج منها أسطحاً باهتة.
|
||||||
|
// نثبّت الأساسي والثانوي صراحةً ونترك للمشتقّ باقي الأدوار — فيبقى الشكل
|
||||||
|
// قريباً من سيرو مع ضمان التباين الذي يوفّره M3.
|
||||||
final scheme = ColorScheme.fromSeed(
|
final scheme = ColorScheme.fromSeed(
|
||||||
seedColor: BuildConfig.seedColor,
|
seedColor: BuildConfig.seedColor,
|
||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
|
).copyWith(
|
||||||
|
primary: brightness == Brightness.dark
|
||||||
|
? BuildConfig.accentColor
|
||||||
|
: BuildConfig.seedColor,
|
||||||
|
onPrimary: brightness == Brightness.dark
|
||||||
|
? BuildConfig.seedColor
|
||||||
|
: Colors.white,
|
||||||
|
secondary: BuildConfig.accentColor,
|
||||||
|
surface: brightness == Brightness.dark
|
||||||
|
? const Color(0xFF161618)
|
||||||
|
: const Color(0xFFF8FAFC),
|
||||||
|
onSurface: brightness == Brightness.dark
|
||||||
|
? Colors.white
|
||||||
|
: const Color(0xFF0F172A),
|
||||||
|
onSurfaceVariant: brightness == Brightness.dark
|
||||||
|
? const Color(0xFFB0B3B8)
|
||||||
|
: const Color(0xFF64748B),
|
||||||
|
outlineVariant: brightness == Brightness.dark
|
||||||
|
? const Color(0xFF3A3A3C)
|
||||||
|
: const Color(0xFFE2E8F0),
|
||||||
);
|
);
|
||||||
final isDark = brightness == Brightness.dark;
|
final isDark = brightness == Brightness.dark;
|
||||||
final texts = buildTextTheme(brightness, locale);
|
final texts = buildTextTheme(brightness, locale);
|
||||||
|
|||||||
@@ -40,35 +40,37 @@ class TripzColors extends ThemeExtension<TripzColors> {
|
|||||||
/// لون هيكل التحميل (skeleton) — لا spinner فارغ (docs/26 §8.4).
|
/// لون هيكل التحميل (skeleton) — لا spinner فارغ (docs/26 §8.4).
|
||||||
final Color skeleton;
|
final Color skeleton;
|
||||||
|
|
||||||
|
// الأرقام من نظام سيرو الميداني: نجاح `#22C55E` · تحذير `#F59E0B` ·
|
||||||
|
// خطأ `#EF4444` — مألوفة لمستخدمي سيرو ومجرَّبة على شاشات الشمس.
|
||||||
static const light = TripzColors(
|
static const light = TripzColors(
|
||||||
success: Color(0xFF1B8A5A),
|
success: Color(0xFF22C55E),
|
||||||
onSuccess: Color(0xFFFFFFFF),
|
onSuccess: Color(0xFFFFFFFF),
|
||||||
warning: Color(0xFFB4690E),
|
warning: Color(0xFFF59E0B),
|
||||||
onWarning: Color(0xFFFFFFFF),
|
onWarning: Color(0xFF2A1A00),
|
||||||
danger: Color(0xFFC5303B),
|
danger: Color(0xFFEF4444),
|
||||||
onDanger: Color(0xFFFFFFFF),
|
onDanger: Color(0xFFFFFFFF),
|
||||||
info: Color(0xFF2563A8),
|
info: Color(0xFF8C9CF8),
|
||||||
surfaceRaised: Color(0xFFFFFFFF),
|
surfaceRaised: Color(0xFFFFFFFF),
|
||||||
mapRoute: Color(0xFF1F6FEB),
|
mapRoute: Color(0xFF1A2340),
|
||||||
mapOrigin: Color(0xFF1B8A5A),
|
mapOrigin: Color(0xFF22C55E),
|
||||||
mapDestination: Color(0xFFC5303B),
|
mapDestination: Color(0xFFEF4444),
|
||||||
skeleton: Color(0xFFE6E8EC),
|
skeleton: Color(0xFFE2E8F0),
|
||||||
);
|
);
|
||||||
|
|
||||||
static const dark = TripzColors(
|
static const dark = TripzColors(
|
||||||
success: Color(0xFF4ECB8D),
|
success: Color(0xFF4ADE80),
|
||||||
onSuccess: Color(0xFF00281A),
|
onSuccess: Color(0xFF00281A),
|
||||||
warning: Color(0xFFE9A63B),
|
warning: Color(0xFFFBBF24),
|
||||||
onWarning: Color(0xFF2A1A00),
|
onWarning: Color(0xFF2A1A00),
|
||||||
danger: Color(0xFFF2707A),
|
danger: Color(0xFFF87171),
|
||||||
onDanger: Color(0xFF33000A),
|
onDanger: Color(0xFF33000A),
|
||||||
info: Color(0xFF7FB2F0),
|
info: Color(0xFF8C9CF8),
|
||||||
// ليس أسود صرفاً — رمادي داكن متدرّج.
|
// ليس أسود صرفاً — رمادي داكن متدرّج (بطاقة سيرو `#2C2C2E`).
|
||||||
surfaceRaised: Color(0xFF20232A),
|
surfaceRaised: Color(0xFF2C2C2E),
|
||||||
mapRoute: Color(0xFF5B9BFF),
|
mapRoute: Color(0xFF8C9CF8),
|
||||||
mapOrigin: Color(0xFF4ECB8D),
|
mapOrigin: Color(0xFF4ADE80),
|
||||||
mapDestination: Color(0xFFF2707A),
|
mapDestination: Color(0xFFF87171),
|
||||||
skeleton: Color(0xFF2C3038),
|
skeleton: Color(0xFF3A3A3C),
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,57 +1,33 @@
|
|||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../features/auth/cubit/login_cubit.dart';
|
import '../features/auth/cubit/login_cubit.dart';
|
||||||
|
|
||||||
import '../features/auth/data/auth_repository.dart';
|
import '../features/auth/data/auth_repository.dart';
|
||||||
import '../features/chat/cubit/chat_cubit.dart';
|
import '../features/chat/cubit/chat_cubit.dart';
|
||||||
import '../features/chat/data/chat_repository.dart';
|
import '../features/chat/data/chat_repository.dart';
|
||||||
import '../features/profile/data/profile_repository.dart';
|
import '../features/profile/data/profile_repository.dart';
|
||||||
|
|
||||||
import '../features/rating/cubit/rating_cubit.dart';
|
import '../features/rating/cubit/rating_cubit.dart';
|
||||||
|
|
||||||
import '../features/rating/data/rating_repository.dart';
|
import '../features/rating/data/rating_repository.dart';
|
||||||
|
|
||||||
import '../features/settings/cubit/settings_cubit.dart';
|
import '../features/settings/cubit/settings_cubit.dart';
|
||||||
|
|
||||||
import '../features/trip/cubit/history_cubit.dart';
|
import '../features/trip/cubit/history_cubit.dart';
|
||||||
|
|
||||||
import '../features/trip/cubit/ride_cubit.dart';
|
import '../features/trip/cubit/ride_cubit.dart';
|
||||||
|
|
||||||
import '../features/trip/data/history_repository.dart';
|
import '../features/trip/data/history_repository.dart';
|
||||||
|
|
||||||
import '../features/trip/data/maps_repository.dart';
|
import '../features/trip/data/maps_repository.dart';
|
||||||
|
|
||||||
import '../features/trip/data/trip_repository.dart';
|
import '../features/trip/data/trip_repository.dart';
|
||||||
|
|
||||||
import '../features/wallet/cubit/wallet_cubit.dart';
|
import '../features/wallet/cubit/wallet_cubit.dart';
|
||||||
|
|
||||||
import '../features/wallet/data/wallet_repository.dart';
|
import '../features/wallet/data/wallet_repository.dart';
|
||||||
|
|
||||||
import 'api/antlaq_api.dart';
|
import 'api/antlaq_api.dart';
|
||||||
|
|
||||||
import 'api/api_client.dart';
|
import 'api/api_client.dart';
|
||||||
|
|
||||||
import 'build_config.dart';
|
import 'build_config.dart';
|
||||||
|
|
||||||
import 'location/location_service.dart';
|
import 'location/location_service.dart';
|
||||||
|
|
||||||
import 'notifications/push_service.dart';
|
import 'notifications/push_service.dart';
|
||||||
|
|
||||||
import 'realtime/realtime_service.dart';
|
import 'realtime/realtime_service.dart';
|
||||||
|
|
||||||
import 'session/session_cubit.dart';
|
import 'session/session_cubit.dart';
|
||||||
import 'session/session_source.dart';
|
import 'session/session_source.dart';
|
||||||
|
|
||||||
import 'storage/token_store.dart';
|
import 'storage/token_store.dart';
|
||||||
|
|
||||||
import 'tenant/tenant_cubit.dart';
|
import 'tenant/tenant_cubit.dart';
|
||||||
|
|
||||||
final sl = GetIt.instance;
|
final sl = GetIt.instance;
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
|
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import '../api/api_client.dart';
|
import '../api/api_client.dart';
|
||||||
|
|
||||||
import '../api/api_exception.dart';
|
import '../api/api_exception.dart';
|
||||||
|
|
||||||
/// إشعارات FCM — **الطبقة الوحيدة التي تعرف Firebase**.
|
/// إشعارات FCM — **الطبقة الوحيدة التي تعرف Firebase**.
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/// مفاتيح الدول المدعومة.
|
||||||
|
///
|
||||||
|
/// حزمة `intl_phone_field` التي يستعملها سيرو تحمل **240 دولة** ولا نحتاج
|
||||||
|
/// منها إلا أربعاً، وتجرّ معها أصولاً وتبعيّات. القائمة هنا صريحة ومقصورة
|
||||||
|
/// على أسواقنا — وتتوسّع بسطر واحد.
|
||||||
|
///
|
||||||
|
/// ترتيب العرض يبدأ بحزمة دولة المستأجر (`countryPack`)، فالمشغّل الأردني
|
||||||
|
/// يرى الأردن أولاً بلا بحث.
|
||||||
|
enum CountryCode {
|
||||||
|
jordan('jo', '962', 'الأردن', 'Jordan', '🇯🇴', 9),
|
||||||
|
syria('sy', '963', 'سوريا', 'Syria', '🇸🇾', 9),
|
||||||
|
egypt('eg', '20', 'مصر', 'Egypt', '🇪🇬', 10),
|
||||||
|
iraq('iq', '964', 'العراق', 'Iraq', '🇮🇶', 10);
|
||||||
|
|
||||||
|
const CountryCode(
|
||||||
|
this.pack,
|
||||||
|
this.dial,
|
||||||
|
this.nameAr,
|
||||||
|
this.nameEn,
|
||||||
|
this.flag,
|
||||||
|
this.nationalLength,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// يطابق `countryPack` في إعداد المستأجر.
|
||||||
|
final String pack;
|
||||||
|
|
||||||
|
/// المفتاح الدولي بلا `+`.
|
||||||
|
final String dial;
|
||||||
|
|
||||||
|
final String nameAr;
|
||||||
|
final String nameEn;
|
||||||
|
final String flag;
|
||||||
|
|
||||||
|
/// طول الرقم الوطني **بلا الصفر البادئ** — أساس التحقّق المحلّي.
|
||||||
|
final int nationalLength;
|
||||||
|
|
||||||
|
String nameFor(String languageCode) =>
|
||||||
|
languageCode == 'ar' ? nameAr : nameEn;
|
||||||
|
|
||||||
|
static CountryCode fromPack(String pack) =>
|
||||||
|
values.firstWhere((c) => c.pack == pack, orElse: () => jordan);
|
||||||
|
|
||||||
|
/// المستأجر أولاً ثم الباقي — لا ترتيب أبجدي يدفن سوق المشغّل للأسفل.
|
||||||
|
static List<CountryCode> orderedFor(String pack) {
|
||||||
|
final first = fromPack(pack);
|
||||||
|
return [first, ...values.where((c) => c != first)];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,8 @@ enum PaymentChannel {
|
|||||||
mtn('mtn_cash', 'MTN Cash', 'إم تي إن كاش'),
|
mtn('mtn_cash', 'MTN Cash', 'إم تي إن كاش'),
|
||||||
shamCash('sham_cash', 'Sham Cash', 'شام كاش'),
|
shamCash('sham_cash', 'Sham Cash', 'شام كاش'),
|
||||||
paymob('paymob', 'PayMob', 'باي موب'),
|
paymob('paymob', 'PayMob', 'باي موب'),
|
||||||
|
zainCash('zain_cash', 'ZainCash', 'زين كاش'),
|
||||||
|
qiCard('qi_card', 'Qi Card', 'كي كارد'),
|
||||||
cash('cash', 'Cash', 'نقداً');
|
cash('cash', 'Cash', 'نقداً');
|
||||||
|
|
||||||
const PaymentChannel(this.wire, this.nameEn, this.nameAr);
|
const PaymentChannel(this.wire, this.nameEn, this.nameAr);
|
||||||
@@ -38,6 +40,14 @@ enum PaymentChannel {
|
|||||||
PaymentChannel.cash,
|
PaymentChannel.cash,
|
||||||
],
|
],
|
||||||
'eg' => const [PaymentChannel.paymob, PaymentChannel.cash],
|
'eg' => const [PaymentChannel.paymob, PaymentChannel.cash],
|
||||||
|
// العراق: المحافظ الأشيع محلياً. ⚠️ الباك إند لا يعرف حزمة `iq` بعد
|
||||||
|
// (`default-tariffs.ts` فيه jo/sy/eg فقط) — مستأجر عراقي يُزوَّد بلا
|
||||||
|
// تعرفة، وهذا شغل خادم يسبق أي إطلاق عراقي.
|
||||||
|
'iq' => const [
|
||||||
|
PaymentChannel.zainCash,
|
||||||
|
PaymentChannel.qiCard,
|
||||||
|
PaymentChannel.cash,
|
||||||
|
],
|
||||||
// حزمة غير معروفة: النقد وحده — لا نعرض قناة قد لا تعمل في سوقها.
|
// حزمة غير معروفة: النقد وحده — لا نعرض قناة قد لا تعمل في سوقها.
|
||||||
_ => const [PaymentChannel.cash],
|
_ => const [PaymentChannel.cash],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,36 +1,22 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../features/auth/view/login_page.dart';
|
import '../features/auth/view/login_page.dart';
|
||||||
|
|
||||||
import '../features/chat/view/chat_page.dart';
|
import '../features/chat/view/chat_page.dart';
|
||||||
import '../features/profile/view/profile_edit_page.dart';
|
import '../features/profile/view/profile_edit_page.dart';
|
||||||
|
|
||||||
import '../features/profile/view/profile_setup_page.dart';
|
import '../features/profile/view/profile_setup_page.dart';
|
||||||
|
|
||||||
import '../features/rating/data/rating_repository.dart';
|
import '../features/rating/data/rating_repository.dart';
|
||||||
|
|
||||||
import '../features/rating/view/rating_page.dart';
|
import '../features/rating/view/rating_page.dart';
|
||||||
|
|
||||||
import '../features/settings/view/settings_page.dart';
|
import '../features/settings/view/settings_page.dart';
|
||||||
|
|
||||||
import '../features/splash/view/splash_page.dart';
|
import '../features/splash/view/splash_page.dart';
|
||||||
|
|
||||||
import '../features/support/view/support_page.dart';
|
import '../features/support/view/support_page.dart';
|
||||||
|
|
||||||
import '../features/trip/view/history_page.dart';
|
import '../features/trip/view/history_page.dart';
|
||||||
|
|
||||||
import '../features/trip/view/ride_page.dart';
|
import '../features/trip/view/ride_page.dart';
|
||||||
|
|
||||||
import '../features/wallet/view/wallet_page.dart';
|
import '../features/wallet/view/wallet_page.dart';
|
||||||
|
|
||||||
import 'di.dart';
|
import 'di.dart';
|
||||||
|
|
||||||
import 'session/session_cubit.dart';
|
import 'session/session_cubit.dart';
|
||||||
|
|
||||||
import 'session/session_state.dart';
|
import 'session/session_state.dart';
|
||||||
|
|
||||||
class Routes {
|
class Routes {
|
||||||
|
|||||||
@@ -3,11 +3,8 @@ import 'dart:async';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../api/api_exception.dart';
|
import '../api/api_exception.dart';
|
||||||
|
|
||||||
import 'app_user.dart';
|
import 'app_user.dart';
|
||||||
|
|
||||||
import 'session_source.dart';
|
import 'session_source.dart';
|
||||||
|
|
||||||
import 'session_state.dart';
|
import 'session_state.dart';
|
||||||
|
|
||||||
/// حالة الجلسة على مستوى التطبيق — مصدر قرار التوجيه الوحيد
|
/// حالة الجلسة على مستوى التطبيق — مصدر قرار التوجيه الوحيد
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import 'tenant_cubit.dart';
|
import 'tenant_cubit.dart';
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../api/api_client.dart';
|
import '../api/api_client.dart';
|
||||||
|
|
||||||
import '../api/api_exception.dart';
|
import '../api/api_exception.dart';
|
||||||
|
|
||||||
import '../build_config.dart';
|
import '../build_config.dart';
|
||||||
|
|
||||||
import 'tenant_config.dart';
|
import 'tenant_config.dart';
|
||||||
|
|
||||||
/// يجلب إعداد المستأجر مرة عند الإقلاع، ويخزّنه محلياً.
|
/// يجلب إعداد المستأجر مرة عند الإقلاع، ويخزّنه محلياً.
|
||||||
|
|||||||
@@ -1,25 +1,15 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../build_config.dart';
|
import '../build_config.dart';
|
||||||
|
|
||||||
import '../config.dart';
|
import '../config.dart';
|
||||||
|
|
||||||
import '../design/tokens.dart';
|
import '../design/tokens.dart';
|
||||||
|
|
||||||
import '../design/tripz_colors.dart';
|
import '../design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
|
|
||||||
import '../router.dart';
|
import '../router.dart';
|
||||||
|
|
||||||
import '../session/session_cubit.dart';
|
import '../session/session_cubit.dart';
|
||||||
|
|
||||||
import '../session/session_state.dart';
|
import '../session/session_state.dart';
|
||||||
|
|
||||||
import '../tenant/feature_gate.dart';
|
import '../tenant/feature_gate.dart';
|
||||||
|
|
||||||
/// الدراور — نقطة تطبيق **طبقتَي الميزات** معاً (docs/23 §5 + docs/38 §10):
|
/// الدراور — نقطة تطبيق **طبقتَي الميزات** معاً (docs/23 §5 + docs/38 §10):
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../design/tokens.dart';
|
import '../design/tokens.dart';
|
||||||
|
|
||||||
import '../design/tripz_colors.dart';
|
import '../design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
|
|
||||||
import 'tripz_button.dart';
|
import 'tripz_button.dart';
|
||||||
|
|
||||||
/// حالة الفراغ: رسالة **تدلّ على الفعل التالي** لا مجرّد «لا يوجد»
|
/// حالة الفراغ: رسالة **تدلّ على الفعل التالي** لا مجرّد «لا يوجد»
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../design/tokens.dart';
|
import '../design/tokens.dart';
|
||||||
|
|
||||||
import '../design/tripz_colors.dart';
|
import '../design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
|
|
||||||
import 'tripz_button.dart';
|
import 'tripz_button.dart';
|
||||||
|
|
||||||
/// حالة الخطأ: رسالة **بالعربية** مع «أعد المحاولة» — لا شاشة صامتة
|
/// حالة الخطأ: رسالة **بالعربية** مع «أعد المحاولة» — لا شاشة صامتة
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../features/auth/cubit/phone_error.dart';
|
import '../../features/auth/cubit/phone_error.dart';
|
||||||
|
|
||||||
import '../api/api_failure.dart';
|
import '../api/api_failure.dart';
|
||||||
|
|
||||||
import '../design/tokens.dart';
|
import '../design/tokens.dart';
|
||||||
|
|
||||||
import '../design/tripz_colors.dart';
|
import '../design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
|
|
||||||
/// النقطة **الوحيدة** التي تُترجَم فيها رموز الفشل إلى نصّ.
|
/// النقطة **الوحيدة** التي تُترجَم فيها رموز الفشل إلى نصّ.
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../design/tokens.dart';
|
import '../design/tokens.dart';
|
||||||
|
|
||||||
import '../design/tripz_colors.dart';
|
import '../design/tripz_colors.dart';
|
||||||
|
|
||||||
/// ترويسة موحّدة لشاشات الدخول: أيقونة ثم عنوان ثم سطر شارح.
|
/// ترويسة موحّدة لشاشات الدخول: أيقونة ثم عنوان ثم سطر شارح.
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import '../design/tokens.dart';
|
||||||
|
import '../design/tripz_colors.dart';
|
||||||
|
import '../design/typography.dart';
|
||||||
|
import '../payments/country_codes.dart';
|
||||||
|
|
||||||
|
/// حقل الهاتف بمفتاح دولة — النمط المجرَّب في سيرو.
|
||||||
|
///
|
||||||
|
/// المفتاح يُختار من ورقة سفلية لا من قائمة منسدلة: الأصابع على الهاتف
|
||||||
|
/// أدقّ مع أهداف كبيرة، والقائمة أربع دول لا مئتان.
|
||||||
|
///
|
||||||
|
/// يُخرج الرقم **كاملاً بالمفتاح بلا `+`** (`962790000001`) — وهو ما يقبله
|
||||||
|
/// الخادم ويطبّعه (docs/38 §2).
|
||||||
|
class PhoneField extends StatelessWidget {
|
||||||
|
const PhoneField({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
required this.country,
|
||||||
|
required this.onCountryChanged,
|
||||||
|
this.label,
|
||||||
|
this.hint,
|
||||||
|
this.errorText,
|
||||||
|
this.enabled = true,
|
||||||
|
this.onSubmitted,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TextEditingController controller;
|
||||||
|
final CountryCode country;
|
||||||
|
final ValueChanged<CountryCode> onCountryChanged;
|
||||||
|
final String? label;
|
||||||
|
final String? hint;
|
||||||
|
final String? errorText;
|
||||||
|
final bool enabled;
|
||||||
|
final ValueChanged<String>? onSubmitted;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TextField(
|
||||||
|
controller: controller,
|
||||||
|
enabled: enabled,
|
||||||
|
autofocus: true,
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
onSubmitted: onSubmitted,
|
||||||
|
maxLength: country.nationalLength + 1,
|
||||||
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
|
autofillHints: const [AutofillHints.telephoneNumber],
|
||||||
|
style: numericStyle(context.texts.bodyLarge),
|
||||||
|
// الرقم يُقرأ يساراً-ليميناً حتى في واجهة عربية.
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: label,
|
||||||
|
hintText: hint,
|
||||||
|
errorText: errorText,
|
||||||
|
counterText: '',
|
||||||
|
prefixIcon: _DialPrefix(
|
||||||
|
country: country,
|
||||||
|
enabled: enabled,
|
||||||
|
onTap: () => _pickCountry(context),
|
||||||
|
),
|
||||||
|
prefixIconConstraints: const BoxConstraints(minWidth: 0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickCountry(BuildContext context) async {
|
||||||
|
final lang = Localizations.localeOf(context).languageCode;
|
||||||
|
final picked = await showModalBottomSheet<CountryCode>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
for (final c in CountryCode.values)
|
||||||
|
ListTile(
|
||||||
|
leading: Text(c.flag, style: const TextStyle(fontSize: 24)),
|
||||||
|
title: Text(c.nameFor(lang)),
|
||||||
|
trailing: Text(
|
||||||
|
'+${c.dial}',
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
style: numericStyle(context.texts.bodyMedium),
|
||||||
|
),
|
||||||
|
selected: c == country,
|
||||||
|
onTap: () => Navigator.of(context).pop(c),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (picked != null) onCountryChanged(picked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DialPrefix extends StatelessWidget {
|
||||||
|
const _DialPrefix({
|
||||||
|
required this.country,
|
||||||
|
required this.enabled,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final CountryCode country;
|
||||||
|
final bool enabled;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: enabled ? onTap : null,
|
||||||
|
borderRadius: Radii.field_,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: Space.sm),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
children: [
|
||||||
|
Text(country.flag, style: const TextStyle(fontSize: 20)),
|
||||||
|
const SizedBox(width: Space.xxs),
|
||||||
|
Text(
|
||||||
|
'+${country.dial}',
|
||||||
|
style: numericStyle(context.texts.bodyLarge),
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.arrow_drop_down_rounded,
|
||||||
|
color: context.colors.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
const SizedBox(width: Space.xxs),
|
||||||
|
SizedBox(
|
||||||
|
height: 24,
|
||||||
|
child: VerticalDivider(
|
||||||
|
width: 1,
|
||||||
|
color: context.colors.outlineVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../design/tokens.dart';
|
import '../design/tokens.dart';
|
||||||
|
|
||||||
import '../design/tripz_colors.dart';
|
import '../design/tripz_colors.dart';
|
||||||
|
|
||||||
/// هيكل تحميل — **لا spinner فارغ** (docs/26 §8.4).
|
/// هيكل تحميل — **لا spinner فارغ** (docs/26 §8.4).
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../design/tokens.dart';
|
import '../design/tokens.dart';
|
||||||
|
|
||||||
import '../design/tripz_colors.dart';
|
import '../design/tripz_colors.dart';
|
||||||
|
|
||||||
enum BannerTone { info, success, warning, danger }
|
enum BannerTone { info, success, warning, danger }
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import '../design/tokens.dart';
|
import '../design/tokens.dart';
|
||||||
|
|
||||||
import '../design/typography.dart';
|
import '../design/typography.dart';
|
||||||
|
|
||||||
/// حقل رمز التحقّق — أربع خانات (طول رمز تريبز، docs/38 §2).
|
/// حقل رمز التحقّق — أربع خانات (طول رمز تريبز، docs/38 §2).
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../design/tokens.dart';
|
import '../design/tokens.dart';
|
||||||
|
|
||||||
import 'empty_view.dart';
|
import 'empty_view.dart';
|
||||||
|
|
||||||
import 'error_view.dart';
|
import 'error_view.dart';
|
||||||
|
|
||||||
import 'skeleton.dart';
|
import 'skeleton.dart';
|
||||||
|
|
||||||
import 'view_status.dart';
|
import 'view_status.dart';
|
||||||
|
|
||||||
/// نقطة التنظيم المركزية (docs/26 §6). **ممنوع `Scaffold` خام في
|
/// نقطة التنظيم المركزية (docs/26 §6). **ممنوع `Scaffold` خام في
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import '../design/tokens.dart';
|
import '../design/tokens.dart';
|
||||||
|
|
||||||
import '../design/typography.dart';
|
import '../design/typography.dart';
|
||||||
|
|
||||||
/// حقل الإدخال الموحّد (docs/26 §7): نصّ · هاتف · بحث.
|
/// حقل الإدخال الموحّد (docs/26 §7): نصّ · هاتف · بحث.
|
||||||
|
|||||||
@@ -1,22 +1,16 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_exception.dart';
|
import '../../../core/api/api_exception.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_failure.dart';
|
import '../../../core/api/api_failure.dart';
|
||||||
|
|
||||||
import '../../../core/config.dart';
|
import '../../../core/config.dart';
|
||||||
|
import '../../../core/payments/country_codes.dart';
|
||||||
import '../data/auth_repository.dart';
|
import '../data/auth_repository.dart';
|
||||||
|
|
||||||
import 'login_state.dart';
|
import 'login_state.dart';
|
||||||
|
|
||||||
import 'phone_error.dart';
|
import 'phone_error.dart';
|
||||||
|
|
||||||
/// تدفّق الدخول كاملاً: الشروط ← إذن الموقع ← الهاتف ← الرمز.
|
/// تدفّق الدخول كاملاً: الشروط ← إذن الموقع ← الهاتف ← الرمز.
|
||||||
@@ -121,6 +115,15 @@ class LoginCubit extends Cubit<LoginState> {
|
|||||||
|
|
||||||
// ── الهاتف ─────────────────────────────────────────────────────────────
|
// ── الهاتف ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// يُنادى عند الإقلاع بحزمة دولة المستأجر — فيرى الأردنيُّ الأردن أولاً.
|
||||||
|
void applyTenantCountry(String countryPack) {
|
||||||
|
if (state.phone.isNotEmpty) return; // لا نغيّر اختياراً واعياً للمستخدم
|
||||||
|
emit(state.copyWith(country: CountryCode.fromPack(countryPack)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void countryChanged(CountryCode country) =>
|
||||||
|
emit(state.copyWith(country: country, clearPhoneError: true));
|
||||||
|
|
||||||
void phoneChanged(String value) {
|
void phoneChanged(String value) {
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
phone: value,
|
phone: value,
|
||||||
@@ -130,14 +133,14 @@ class LoginCubit extends Cubit<LoginState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> submitPhone() async {
|
Future<void> submitPhone() async {
|
||||||
final error = validatePhone(state.phone);
|
final error = validatePhone(state.phone, state.country);
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
emit(state.copyWith(phoneError: error));
|
emit(state.copyWith(phoneError: error));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
emit(state.copyWith(status: LoginStatus.submitting, clearFailure: true));
|
emit(state.copyWith(status: LoginStatus.submitting, clearFailure: true));
|
||||||
try {
|
try {
|
||||||
await _repo.sendOtp(state.phone.trim());
|
await _repo.sendOtp(state.e164);
|
||||||
emit(state.copyWith(status: LoginStatus.idle, step: LoginStep.otp));
|
emit(state.copyWith(status: LoginStatus.idle, step: LoginStep.otp));
|
||||||
_startResendCountdown();
|
_startResendCountdown();
|
||||||
} on ApiException catch (e) {
|
} on ApiException catch (e) {
|
||||||
@@ -164,7 +167,7 @@ class LoginCubit extends Cubit<LoginState> {
|
|||||||
if (!state.canResend) return;
|
if (!state.canResend) return;
|
||||||
emit(state.copyWith(status: LoginStatus.submitting, clearFailure: true));
|
emit(state.copyWith(status: LoginStatus.submitting, clearFailure: true));
|
||||||
try {
|
try {
|
||||||
await _repo.sendOtp(state.phone.trim());
|
await _repo.sendOtp(state.e164);
|
||||||
emit(state.copyWith(status: LoginStatus.idle));
|
emit(state.copyWith(status: LoginStatus.idle));
|
||||||
_startResendCountdown();
|
_startResendCountdown();
|
||||||
} on ApiException catch (e) {
|
} on ApiException catch (e) {
|
||||||
@@ -182,7 +185,7 @@ class LoginCubit extends Cubit<LoginState> {
|
|||||||
if (code.length != AppConfig.otpLength) return;
|
if (code.length != AppConfig.otpLength) return;
|
||||||
emit(state.copyWith(status: LoginStatus.submitting, clearFailure: true));
|
emit(state.copyWith(status: LoginStatus.submitting, clearFailure: true));
|
||||||
try {
|
try {
|
||||||
final user = await _repo.verifyOtp(phone: state.phone.trim(), code: code);
|
final user = await _repo.verifyOtp(phone: state.e164, code: code);
|
||||||
_resendTimer?.cancel();
|
_resendTimer?.cancel();
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
status: LoginStatus.success,
|
status: LoginStatus.success,
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_failure.dart';
|
import '../../../core/api/api_failure.dart';
|
||||||
|
import '../../../core/payments/country_codes.dart';
|
||||||
import '../../../core/session/app_user.dart';
|
import '../../../core/session/app_user.dart';
|
||||||
|
|
||||||
import 'phone_error.dart';
|
import 'phone_error.dart';
|
||||||
|
|
||||||
/// خطوات الدخول بالترتيب الميداني المثبت في سيرو (docs/39 §2): بوابتان
|
/// خطوات الدخول بالترتيب الميداني المثبت في سيرو (docs/39 §2): بوابتان
|
||||||
@@ -17,6 +16,7 @@ class LoginState extends Equatable {
|
|||||||
this.step = LoginStep.agreement,
|
this.step = LoginStep.agreement,
|
||||||
this.status = LoginStatus.idle,
|
this.status = LoginStatus.idle,
|
||||||
this.phone = '',
|
this.phone = '',
|
||||||
|
this.country = CountryCode.jordan,
|
||||||
this.agreed = false,
|
this.agreed = false,
|
||||||
this.failure,
|
this.failure,
|
||||||
this.phoneError,
|
this.phoneError,
|
||||||
@@ -31,6 +31,13 @@ class LoginState extends Equatable {
|
|||||||
/// الرقم كما أدخله المستخدم — الخادم يطبّعه، فلا نطبّعه نحن (docs/38 §2).
|
/// الرقم كما أدخله المستخدم — الخادم يطبّعه، فلا نطبّعه نحن (docs/38 §2).
|
||||||
final String phone;
|
final String phone;
|
||||||
|
|
||||||
|
/// الدولة المختارة — تحدّد المفتاح الدولي وطول التحقّق. تُبدأ من
|
||||||
|
/// `countryPack` المستأجر لا من لغة الجهاز.
|
||||||
|
final CountryCode country;
|
||||||
|
|
||||||
|
/// ما يُرسل للخادم: المفتاح + الرقم بلا `+` (`962790000001`).
|
||||||
|
String get e164 => '${country.dial}${phone.trim()}';
|
||||||
|
|
||||||
final bool agreed;
|
final bool agreed;
|
||||||
final ApiFailure? failure;
|
final ApiFailure? failure;
|
||||||
final PhoneError? phoneError;
|
final PhoneError? phoneError;
|
||||||
@@ -51,6 +58,7 @@ class LoginState extends Equatable {
|
|||||||
LoginStep? step,
|
LoginStep? step,
|
||||||
LoginStatus? status,
|
LoginStatus? status,
|
||||||
String? phone,
|
String? phone,
|
||||||
|
CountryCode? country,
|
||||||
bool? agreed,
|
bool? agreed,
|
||||||
ApiFailure? failure,
|
ApiFailure? failure,
|
||||||
PhoneError? phoneError,
|
PhoneError? phoneError,
|
||||||
@@ -64,6 +72,7 @@ class LoginState extends Equatable {
|
|||||||
step: step ?? this.step,
|
step: step ?? this.step,
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
phone: phone ?? this.phone,
|
phone: phone ?? this.phone,
|
||||||
|
country: country ?? this.country,
|
||||||
agreed: agreed ?? this.agreed,
|
agreed: agreed ?? this.agreed,
|
||||||
failure: clearFailure ? null : (failure ?? this.failure),
|
failure: clearFailure ? null : (failure ?? this.failure),
|
||||||
phoneError: clearPhoneError ? null : (phoneError ?? this.phoneError),
|
phoneError: clearPhoneError ? null : (phoneError ?? this.phoneError),
|
||||||
@@ -79,6 +88,7 @@ class LoginState extends Equatable {
|
|||||||
step,
|
step,
|
||||||
status,
|
status,
|
||||||
phone,
|
phone,
|
||||||
|
country,
|
||||||
agreed,
|
agreed,
|
||||||
failure,
|
failure,
|
||||||
phoneError,
|
phoneError,
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
|
import '../../../core/payments/country_codes.dart';
|
||||||
|
|
||||||
/// أخطاء تحقّق **محليّة** لرقم الهاتف — تُكتشف قبل أي نداء شبكة.
|
/// أخطاء تحقّق **محليّة** لرقم الهاتف — تُكتشف قبل أي نداء شبكة.
|
||||||
/// الشروط الثلاثة منقولة من التحقّق المطبَّق ميدانياً في سيرو (docs/39 §2).
|
/// الشروط الثلاثة منقولة من التحقّق المطبَّق ميدانياً في سيرو (docs/39 §2).
|
||||||
enum PhoneError { empty, leadingZero, tooShort }
|
enum PhoneError { empty, leadingZero, tooShort }
|
||||||
|
|
||||||
PhoneError? validatePhone(String raw) {
|
/// الطول المتوقّع من **الدولة المختارة** لا رقماً ثابتاً: الأردن وسوريا 9
|
||||||
|
/// خانات، ومصر والعراق 10.
|
||||||
|
PhoneError? validatePhone(String raw, CountryCode country) {
|
||||||
final v = raw.trim();
|
final v = raw.trim();
|
||||||
if (v.isEmpty) return PhoneError.empty;
|
if (v.isEmpty) return PhoneError.empty;
|
||||||
if (v.startsWith('0')) return PhoneError.leadingZero;
|
if (v.startsWith('0')) return PhoneError.leadingZero;
|
||||||
if (v.length < 9) return PhoneError.tooShort;
|
if (v.length < country.nationalLength) return PhoneError.tooShort;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import '../../../core/api/api_client.dart';
|
import '../../../core/api/api_client.dart';
|
||||||
|
|
||||||
import '../../../core/session/app_user.dart';
|
import '../../../core/session/app_user.dart';
|
||||||
|
|
||||||
import '../../../core/session/session_source.dart';
|
import '../../../core/session/session_source.dart';
|
||||||
|
|
||||||
import '../../../core/storage/token_store.dart';
|
import '../../../core/storage/token_store.dart';
|
||||||
|
|
||||||
/// كل نداءات المصادقة (docs/38 §2). الـCubit لا ينادي Dio مباشرة أبداً
|
/// كل نداءات المصادقة (docs/38 §2). الـCubit لا ينادي Dio مباشرة أبداً
|
||||||
|
|||||||
@@ -1,22 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/design/tokens.dart';
|
import '../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../core/design/tripz_colors.dart';
|
import '../../../core/design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../../../core/l10n/l10n.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 '../../../core/ui/form_header.dart';
|
import '../../../core/ui/form_header.dart';
|
||||||
|
import '../../../core/ui/tripz_button.dart';
|
||||||
|
import '../../../core/ui/tripz_scaffold.dart';
|
||||||
|
import '../cubit/login_cubit.dart';
|
||||||
|
import '../cubit/login_state.dart';
|
||||||
|
|
||||||
/// البوابة الأولى: شاشة كاملة لا حوار، وزر متابعة **معطّل حتى يُؤشَّر**
|
/// البوابة الأولى: شاشة كاملة لا حوار، وزر متابعة **معطّل حتى يُؤشَّر**
|
||||||
/// (docs/39 §2). تُحفظ الموافقة فلا تتكرر.
|
/// (docs/39 §2). تُحفظ الموافقة فلا تتكرر.
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/di.dart';
|
import '../../../core/di.dart';
|
||||||
|
|
||||||
import '../../../core/session/session_cubit.dart';
|
import '../../../core/session/session_cubit.dart';
|
||||||
|
|
||||||
import '../cubit/login_cubit.dart';
|
import '../cubit/login_cubit.dart';
|
||||||
|
|
||||||
import '../cubit/login_state.dart';
|
import '../cubit/login_state.dart';
|
||||||
|
|
||||||
import 'agreement_view.dart';
|
import 'agreement_view.dart';
|
||||||
|
|
||||||
import 'otp_view.dart';
|
import 'otp_view.dart';
|
||||||
|
|
||||||
import 'permission_view.dart';
|
import 'permission_view.dart';
|
||||||
|
|
||||||
import 'phone_view.dart';
|
import 'phone_view.dart';
|
||||||
|
|
||||||
/// مضيف تدفّق الدخول: خطوة واحدة معروضة في كل لحظة، والانتقال بينها من
|
/// مضيف تدفّق الدخول: خطوة واحدة معروضة في كل لحظة، والانتقال بينها من
|
||||||
|
|||||||
@@ -1,26 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/design/tokens.dart';
|
import '../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../core/design/tripz_colors.dart';
|
import '../../../core/design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../../../core/l10n/l10n.dart';
|
import '../../../core/l10n/l10n.dart';
|
||||||
|
|
||||||
import '../../../core/ui/failure_text.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 '../../../core/ui/form_header.dart';
|
import '../../../core/ui/form_header.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';
|
||||||
|
|
||||||
class OtpView extends StatefulWidget {
|
class OtpView extends StatefulWidget {
|
||||||
const OtpView({super.key});
|
const OtpView({super.key});
|
||||||
|
|||||||
@@ -1,22 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/design/tokens.dart';
|
import '../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../core/l10n/l10n.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 '../../../core/ui/form_header.dart';
|
import '../../../core/ui/form_header.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';
|
||||||
|
|
||||||
/// البوابة الثانية: إذن الموقع. عند الرفض النهائي لا يبقى إلا فتح إعدادات
|
/// البوابة الثانية: إذن الموقع. عند الرفض النهائي لا يبقى إلا فتح إعدادات
|
||||||
/// النظام — والعودة منها يلتقطها `LoginPage` بمراقبة دورة الحياة.
|
/// النظام — والعودة منها يلتقطها `LoginPage` بمراقبة دورة الحياة.
|
||||||
|
|||||||
@@ -1,28 +1,48 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/design/tokens.dart';
|
import '../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../core/l10n/l10n.dart';
|
import '../../../core/l10n/l10n.dart';
|
||||||
|
import '../../../core/tenant/tenant_cubit.dart';
|
||||||
import '../../../core/ui/failure_text.dart';
|
import '../../../core/ui/failure_text.dart';
|
||||||
|
import '../../../core/ui/form_header.dart';
|
||||||
|
import '../../../core/ui/phone_field.dart';
|
||||||
import '../../../core/ui/tripz_button.dart';
|
import '../../../core/ui/tripz_button.dart';
|
||||||
|
|
||||||
import '../../../core/ui/tripz_scaffold.dart';
|
import '../../../core/ui/tripz_scaffold.dart';
|
||||||
|
|
||||||
import '../../../core/ui/tripz_text_field.dart';
|
|
||||||
|
|
||||||
import '../cubit/login_cubit.dart';
|
import '../cubit/login_cubit.dart';
|
||||||
|
|
||||||
import '../cubit/login_state.dart';
|
import '../cubit/login_state.dart';
|
||||||
|
|
||||||
import '../../../core/ui/form_header.dart';
|
class PhoneView extends StatefulWidget {
|
||||||
|
|
||||||
class PhoneView extends StatelessWidget {
|
|
||||||
const PhoneView({super.key});
|
const PhoneView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PhoneView> createState() => _PhoneViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PhoneViewState extends State<PhoneView> {
|
||||||
|
final _phone = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_phone.addListener(
|
||||||
|
() => context.read<LoginCubit>().phoneChanged(_phone.text),
|
||||||
|
);
|
||||||
|
// الدولة الافتراضية من المستأجر — يُطبَّق بعد أول إطار كي يتوفّر السياق.
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context
|
||||||
|
.read<LoginCubit>()
|
||||||
|
.applyTenantCountry(context.read<TenantCubit>().state.countryPack);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_phone.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
@@ -30,6 +50,7 @@ class PhoneView extends StatelessWidget {
|
|||||||
buildWhen: (p, n) =>
|
buildWhen: (p, n) =>
|
||||||
p.status != n.status ||
|
p.status != n.status ||
|
||||||
p.phoneError != n.phoneError ||
|
p.phoneError != n.phoneError ||
|
||||||
|
p.country != n.country ||
|
||||||
p.failure != n.failure,
|
p.failure != n.failure,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final cubit = context.read<LoginCubit>();
|
final cubit = context.read<LoginCubit>();
|
||||||
@@ -49,11 +70,13 @@ class PhoneView extends StatelessWidget {
|
|||||||
lead: l10n.phoneLead,
|
lead: l10n.phoneLead,
|
||||||
),
|
),
|
||||||
const SizedBox(height: Space.lg),
|
const SizedBox(height: Space.lg),
|
||||||
TripzTextField.phone(
|
PhoneField(
|
||||||
|
controller: _phone,
|
||||||
|
country: state.country,
|
||||||
|
onCountryChanged: cubit.countryChanged,
|
||||||
label: l10n.phoneLabel,
|
label: l10n.phoneLabel,
|
||||||
hint: l10n.phoneHint,
|
hint: l10n.phoneHint,
|
||||||
enabled: !state.isSubmitting,
|
enabled: !state.isSubmitting,
|
||||||
onChanged: cubit.phoneChanged,
|
|
||||||
onSubmitted: (_) => cubit.submitPhone(),
|
onSubmitted: (_) => cubit.submitPhone(),
|
||||||
errorText: state.phoneError == null
|
errorText: state.phoneError == null
|
||||||
? null
|
? null
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_exception.dart';
|
import '../../../core/api/api_exception.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_failure.dart';
|
import '../../../core/api/api_failure.dart';
|
||||||
|
|
||||||
import '../../../core/session/app_user.dart';
|
import '../../../core/session/app_user.dart';
|
||||||
|
|
||||||
import '../../../core/ui/view_status.dart';
|
import '../../../core/ui/view_status.dart';
|
||||||
|
|
||||||
import '../data/profile_repository.dart';
|
import '../data/profile_repository.dart';
|
||||||
|
|
||||||
class ProfileState {
|
class ProfileState {
|
||||||
|
|||||||
@@ -1,23 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/design/tokens.dart';
|
import '../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../core/design/tripz_colors.dart';
|
import '../../../core/design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../../../core/di.dart';
|
import '../../../core/di.dart';
|
||||||
|
|
||||||
import '../../../core/l10n/l10n.dart';
|
import '../../../core/l10n/l10n.dart';
|
||||||
|
|
||||||
import '../../../core/session/session_cubit.dart';
|
import '../../../core/session/session_cubit.dart';
|
||||||
|
|
||||||
import '../../../core/ui/tripz_button.dart';
|
import '../../../core/ui/tripz_button.dart';
|
||||||
|
|
||||||
import '../../../core/ui/tripz_scaffold.dart';
|
import '../../../core/ui/tripz_scaffold.dart';
|
||||||
|
|
||||||
import '../../../core/ui/tripz_text_field.dart';
|
import '../../../core/ui/tripz_text_field.dart';
|
||||||
|
|
||||||
import '../cubit/profile_cubit.dart';
|
import '../cubit/profile_cubit.dart';
|
||||||
|
|
||||||
/// تعديل الاسم. **الرقم لا يُعدَّل**: هو هوية الحساب على الخادم، وتغييره
|
/// تعديل الاسم. **الرقم لا يُعدَّل**: هو هوية الحساب على الخادم، وتغييره
|
||||||
|
|||||||
@@ -1,25 +1,15 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/design/tokens.dart';
|
import '../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../core/di.dart';
|
import '../../../core/di.dart';
|
||||||
|
|
||||||
import '../../../core/l10n/l10n.dart';
|
import '../../../core/l10n/l10n.dart';
|
||||||
|
|
||||||
import '../../../core/session/session_cubit.dart';
|
import '../../../core/session/session_cubit.dart';
|
||||||
|
|
||||||
import '../../../core/ui/failure_text.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 '../../../core/ui/form_header.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 '../cubit/profile_cubit.dart';
|
||||||
|
|
||||||
class ProfileSetupPage extends StatefulWidget {
|
class ProfileSetupPage extends StatefulWidget {
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_exception.dart';
|
import '../../../core/api/api_exception.dart';
|
||||||
|
|
||||||
import '../data/rating_repository.dart';
|
import '../data/rating_repository.dart';
|
||||||
|
|
||||||
import 'rating_state.dart';
|
import 'rating_state.dart';
|
||||||
|
|
||||||
class RatingCubit extends Cubit<RatingState> {
|
class RatingCubit extends Cubit<RatingState> {
|
||||||
|
|||||||
@@ -1,27 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/design/tokens.dart';
|
import '../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../core/design/tripz_colors.dart';
|
import '../../../core/design/tripz_colors.dart';
|
||||||
|
|
||||||
import '../../../core/di.dart';
|
import '../../../core/di.dart';
|
||||||
|
|
||||||
import '../../../core/l10n/l10n.dart';
|
import '../../../core/l10n/l10n.dart';
|
||||||
|
|
||||||
import '../../../core/ui/tripz_button.dart';
|
import '../../../core/ui/tripz_button.dart';
|
||||||
|
|
||||||
import '../../../core/ui/tripz_scaffold.dart';
|
import '../../../core/ui/tripz_scaffold.dart';
|
||||||
|
|
||||||
import '../../../core/ui/tripz_text_field.dart';
|
import '../../../core/ui/tripz_text_field.dart';
|
||||||
|
|
||||||
import '../cubit/rating_cubit.dart';
|
import '../cubit/rating_cubit.dart';
|
||||||
|
|
||||||
import '../cubit/rating_state.dart';
|
import '../cubit/rating_state.dart';
|
||||||
|
|
||||||
import '../data/rating_repository.dart';
|
import '../data/rating_repository.dart';
|
||||||
|
|
||||||
import 'star_row.dart';
|
import 'star_row.dart';
|
||||||
|
|
||||||
/// شاشة التقييم — **تُفرض عند فتح التطبيق** إن كانت هناك رحلة معلّقة
|
/// شاشة التقييم — **تُفرض عند فتح التطبيق** إن كانت هناك رحلة معلّقة
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../../core/design/tokens.dart';
|
import '../../../core/design/tokens.dart';
|
||||||
|
|
||||||
import '../../../core/design/tripz_colors.dart';
|
import '../../../core/design/tripz_colors.dart';
|
||||||
|
|
||||||
/// خمس نجوم — أهداف لمس ≥ 48 لأن الشاشة تُستعمل عادةً والمستخدم مستعجل.
|
/// خمس نجوم — أهداف لمس ≥ 48 لأن الشاشة تُستعمل عادةً والمستخدم مستعجل.
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import 'settings_state.dart';
|
import 'settings_state.dart';
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user