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:
Hamza-Ayed
2026-08-05 01:08:18 +03:00
co-authored by Claude Opus 5
parent 3a8caf7b27
commit e22cc5c7e4
128 changed files with 684 additions and 666 deletions
-9
View File
@@ -1,23 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'core/build_config.dart';
import 'core/design/theme.dart';
import 'core/di.dart';
import 'core/l10n/l10n.dart';
import 'core/router.dart';
import 'core/tenant/tenant_cubit.dart';
import 'features/settings/cubit/settings_cubit.dart';
import 'features/settings/cubit/settings_state.dart';
class TripzApp extends StatelessWidget {
@@ -1,7 +1,6 @@
import 'package:dio/dio.dart';
import '../config.dart';
import 'api_exception.dart';
/// عميل map-saas — **منفصل تماماً** عن `ApiClient`.
@@ -1,11 +1,8 @@
import 'package:dio/dio.dart';
import '../config.dart';
import '../storage/token_store.dart';
import 'api_exception.dart';
import 'auth_interceptor.dart';
/// الواجهة الوحيدة للخادم. لا تُنشأ `Dio` في أي مكان آخر.
@@ -3,9 +3,7 @@ import 'dart:async';
import 'package:dio/dio.dart';
import '../build_config.dart';
import '../config.dart';
import '../storage/token_store.dart';
/// يثبّت ترويسات كل طلب، ويجدّد التوكن **استباقياً** قبل انتهائه.
+7 -1
View File
@@ -12,7 +12,13 @@ class BuildConfig {
static const String tenantSlug = 'siro';
/// اللون البذرة — منه تُشتقّ `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 splashLogoAsset = 'assets/images/splash_logo.png';
+23 -3
View File
@@ -1,11 +1,8 @@
import 'package:flutter/material.dart';
import '../build_config.dart';
import 'tokens.dart';
import 'tripz_colors.dart';
import 'typography.dart';
/// دالة بناء الثيم الوحيدة (docs/26 §4): مدخلان لا ثالث لهما.
@@ -13,9 +10,32 @@ import 'typography.dart';
/// الاتجاه (RTL/LTR) **لا يُفرض هنا** — `MaterialApp` يشتقّه من الـlocale
/// وحده. فرض `Directionality.rtl` يكسر الإنجليزية (docs/26 §4).
ThemeData buildTheme(Brightness brightness, Locale locale) {
// البذرة الكحلية داكنة جداً، و`fromSeed` وحدها تنتج منها أسطحاً باهتة.
// نثبّت الأساسي والثانوي صراحةً ونترك للمشتقّ باقي الأدوار — فيبقى الشكل
// قريباً من سيرو مع ضمان التباين الذي يوفّره M3.
final scheme = ColorScheme.fromSeed(
seedColor: BuildConfig.seedColor,
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 texts = buildTextTheme(brightness, locale);
@@ -40,35 +40,37 @@ class TripzColors extends ThemeExtension<TripzColors> {
/// لون هيكل التحميل (skeleton) — لا spinner فارغ (docs/26 §8.4).
final Color skeleton;
// الأرقام من نظام سيرو الميداني: نجاح `#22C55E` · تحذير `#F59E0B` ·
// خطأ `#EF4444` — مألوفة لمستخدمي سيرو ومجرَّبة على شاشات الشمس.
static const light = TripzColors(
success: Color(0xFF1B8A5A),
success: Color(0xFF22C55E),
onSuccess: Color(0xFFFFFFFF),
warning: Color(0xFFB4690E),
onWarning: Color(0xFFFFFFFF),
danger: Color(0xFFC5303B),
warning: Color(0xFFF59E0B),
onWarning: Color(0xFF2A1A00),
danger: Color(0xFFEF4444),
onDanger: Color(0xFFFFFFFF),
info: Color(0xFF2563A8),
info: Color(0xFF8C9CF8),
surfaceRaised: Color(0xFFFFFFFF),
mapRoute: Color(0xFF1F6FEB),
mapOrigin: Color(0xFF1B8A5A),
mapDestination: Color(0xFFC5303B),
skeleton: Color(0xFFE6E8EC),
mapRoute: Color(0xFF1A2340),
mapOrigin: Color(0xFF22C55E),
mapDestination: Color(0xFFEF4444),
skeleton: Color(0xFFE2E8F0),
);
static const dark = TripzColors(
success: Color(0xFF4ECB8D),
success: Color(0xFF4ADE80),
onSuccess: Color(0xFF00281A),
warning: Color(0xFFE9A63B),
warning: Color(0xFFFBBF24),
onWarning: Color(0xFF2A1A00),
danger: Color(0xFFF2707A),
danger: Color(0xFFF87171),
onDanger: Color(0xFF33000A),
info: Color(0xFF7FB2F0),
// ليس أسود صرفاً — رمادي داكن متدرّج.
surfaceRaised: Color(0xFF20232A),
mapRoute: Color(0xFF5B9BFF),
mapOrigin: Color(0xFF4ECB8D),
mapDestination: Color(0xFFF2707A),
skeleton: Color(0xFF2C3038),
info: Color(0xFF8C9CF8),
// ليس أسود صرفاً — رمادي داكن متدرّج (بطاقة سيرو `#2C2C2E`).
surfaceRaised: Color(0xFF2C2C2E),
mapRoute: Color(0xFF8C9CF8),
mapOrigin: Color(0xFF4ADE80),
mapDestination: Color(0xFFF87171),
skeleton: Color(0xFF3A3A3C),
);
@override
@@ -1,11 +1,8 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import '../api/api_client.dart';
import '../api/api_exception.dart';
/// إشعارات FCM — **الطبقة الوحيدة التي تعرف Firebase**.
@@ -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', 'إم تي إن كاش'),
shamCash('sham_cash', 'Sham Cash', 'شام كاش'),
paymob('paymob', 'PayMob', 'باي موب'),
zainCash('zain_cash', 'ZainCash', 'زين كاش'),
qiCard('qi_card', 'Qi Card', 'كي كارد'),
cash('cash', 'Cash', 'نقداً');
const PaymentChannel(this.wire, this.nameEn, this.nameAr);
@@ -38,6 +40,14 @@ enum PaymentChannel {
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],
};
@@ -3,11 +3,8 @@ import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../api/api_exception.dart';
import 'app_user.dart';
import 'session_source.dart';
import 'session_state.dart';
/// حالة الجلسة على مستوى التطبيق — مصدر قرار التوجيه الوحيد
@@ -1,5 +1,4 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'tenant_cubit.dart';
@@ -1,13 +1,9 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../api/api_client.dart';
import '../api/api_exception.dart';
import '../build_config.dart';
import 'tenant_config.dart';
/// يجلب إعداد المستأجر مرة عند الإقلاع، ويخزّنه محلياً.
@@ -1,25 +1,15 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../build_config.dart';
import '../config.dart';
import '../design/tokens.dart';
import '../design/tripz_colors.dart';
import '../l10n/l10n.dart';
import '../router.dart';
import '../session/session_cubit.dart';
import '../session/session_state.dart';
import '../tenant/feature_gate.dart';
/// الدراور — نقطة تطبيق **طبقتَي الميزات** معاً (docs/23 §5 + docs/38 §10):
@@ -1,11 +1,8 @@
import 'package:flutter/material.dart';
import '../design/tokens.dart';
import '../design/tripz_colors.dart';
import '../l10n/l10n.dart';
import 'tripz_button.dart';
/// حالة الفراغ: رسالة **تدلّ على الفعل التالي** لا مجرّد «لا يوجد»
@@ -1,11 +1,8 @@
import 'package:flutter/material.dart';
import '../design/tokens.dart';
import '../design/tripz_colors.dart';
import '../l10n/l10n.dart';
import 'tripz_button.dart';
/// حالة الخطأ: رسالة **بالعربية** مع «أعد المحاولة» — لا شاشة صامتة
@@ -1,13 +1,9 @@
import 'package:flutter/material.dart';
import '../../features/auth/cubit/phone_error.dart';
import '../api/api_failure.dart';
import '../design/tokens.dart';
import '../design/tripz_colors.dart';
import '../l10n/l10n.dart';
/// النقطة **الوحيدة** التي تُترجَم فيها رموز الفشل إلى نصّ.
@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import '../design/tokens.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 '../design/tokens.dart';
import '../design/tripz_colors.dart';
/// هيكل تحميل — **لا spinner فارغ** (docs/26 §8.4).
@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import '../design/tokens.dart';
import '../design/tripz_colors.dart';
enum BannerTone { info, success, warning, danger }
@@ -1,9 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../design/tokens.dart';
import '../design/typography.dart';
/// حقل رمز التحقّق — أربع خانات (طول رمز تريبز، docs/38 §2).
@@ -1,13 +1,9 @@
import 'package:flutter/material.dart';
import '../design/tokens.dart';
import 'empty_view.dart';
import 'error_view.dart';
import 'skeleton.dart';
import 'view_status.dart';
/// نقطة التنظيم المركزية (docs/26 §6). **ممنوع `Scaffold` خام في
@@ -1,9 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../design/tokens.dart';
import '../design/typography.dart';
/// حقل الإدخال الموحّد (docs/26 §7): نصّ · هاتف · بحث.
@@ -1,22 +1,16 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:geolocator/geolocator.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../core/api/api_exception.dart';
import '../../../core/api/api_failure.dart';
import '../../../core/config.dart';
import '../../../core/payments/country_codes.dart';
import '../data/auth_repository.dart';
import 'login_state.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) {
emit(state.copyWith(
phone: value,
@@ -130,14 +133,14 @@ class LoginCubit extends Cubit<LoginState> {
}
Future<void> submitPhone() async {
final error = validatePhone(state.phone);
final error = validatePhone(state.phone, state.country);
if (error != null) {
emit(state.copyWith(phoneError: error));
return;
}
emit(state.copyWith(status: LoginStatus.submitting, clearFailure: true));
try {
await _repo.sendOtp(state.phone.trim());
await _repo.sendOtp(state.e164);
emit(state.copyWith(status: LoginStatus.idle, step: LoginStep.otp));
_startResendCountdown();
} on ApiException catch (e) {
@@ -164,7 +167,7 @@ class LoginCubit extends Cubit<LoginState> {
if (!state.canResend) return;
emit(state.copyWith(status: LoginStatus.submitting, clearFailure: true));
try {
await _repo.sendOtp(state.phone.trim());
await _repo.sendOtp(state.e164);
emit(state.copyWith(status: LoginStatus.idle));
_startResendCountdown();
} on ApiException catch (e) {
@@ -182,7 +185,7 @@ class LoginCubit extends Cubit<LoginState> {
if (code.length != AppConfig.otpLength) return;
emit(state.copyWith(status: LoginStatus.submitting, clearFailure: true));
try {
final user = await _repo.verifyOtp(phone: state.phone.trim(), code: code);
final user = await _repo.verifyOtp(phone: state.e164, code: code);
_resendTimer?.cancel();
emit(state.copyWith(
status: LoginStatus.success,
@@ -1,9 +1,8 @@
import 'package:equatable/equatable.dart';
import '../../../core/api/api_failure.dart';
import '../../../core/payments/country_codes.dart';
import '../../../core/session/app_user.dart';
import 'phone_error.dart';
/// خطوات الدخول بالترتيب الميداني المثبت في سيرو (docs/39 §2): بوابتان
@@ -17,6 +16,7 @@ class LoginState extends Equatable {
this.step = LoginStep.agreement,
this.status = LoginStatus.idle,
this.phone = '',
this.country = CountryCode.jordan,
this.agreed = false,
this.failure,
this.phoneError,
@@ -31,6 +31,13 @@ class LoginState extends Equatable {
/// الرقم كما أدخله المستخدم — الخادم يطبّعه، فلا نطبّعه نحن (docs/38 §2).
final String phone;
/// الدولة المختارة — تحدّد المفتاح الدولي وطول التحقّق. تُبدأ من
/// `countryPack` المستأجر لا من لغة الجهاز.
final CountryCode country;
/// ما يُرسل للخادم: المفتاح + الرقم بلا `+` (`962790000001`).
String get e164 => '${country.dial}${phone.trim()}';
final bool agreed;
final ApiFailure? failure;
final PhoneError? phoneError;
@@ -51,6 +58,7 @@ class LoginState extends Equatable {
LoginStep? step,
LoginStatus? status,
String? phone,
CountryCode? country,
bool? agreed,
ApiFailure? failure,
PhoneError? phoneError,
@@ -64,6 +72,7 @@ class LoginState extends Equatable {
step: step ?? this.step,
status: status ?? this.status,
phone: phone ?? this.phone,
country: country ?? this.country,
agreed: agreed ?? this.agreed,
failure: clearFailure ? null : (failure ?? this.failure),
phoneError: clearPhoneError ? null : (phoneError ?? this.phoneError),
@@ -79,6 +88,7 @@ class LoginState extends Equatable {
step,
status,
phone,
country,
agreed,
failure,
phoneError,
@@ -1,11 +1,15 @@
import '../../../core/payments/country_codes.dart';
/// أخطاء تحقّق **محليّة** لرقم الهاتف — تُكتشف قبل أي نداء شبكة.
/// الشروط الثلاثة منقولة من التحقّق المطبَّق ميدانياً في سيرو (docs/39 §2).
enum PhoneError { empty, leadingZero, tooShort }
PhoneError? validatePhone(String raw) {
/// الطول المتوقّع من **الدولة المختارة** لا رقماً ثابتاً: الأردن وسوريا 9
/// خانات، ومصر والعراق 10.
PhoneError? validatePhone(String raw, CountryCode country) {
final v = raw.trim();
if (v.isEmpty) return PhoneError.empty;
if (v.startsWith('0')) return PhoneError.leadingZero;
if (v.length < 9) return PhoneError.tooShort;
if (v.length < country.nationalLength) return PhoneError.tooShort;
return null;
}
@@ -1,9 +1,6 @@
import '../../../core/api/api_client.dart';
import '../../../core/session/app_user.dart';
import '../../../core/session/session_source.dart';
import '../../../core/storage/token_store.dart';
/// كل نداءات المصادقة (docs/38 §2). الـCubit لا ينادي Dio مباشرة أبداً
@@ -1,22 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/design/tokens.dart';
import '../../../core/design/tripz_colors.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/ui/tripz_button.dart';
import '../../../core/ui/tripz_scaffold.dart';
import '../cubit/login_cubit.dart';
import '../cubit/login_state.dart';
import '../../../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). تُحفظ الموافقة فلا تتكرر.
@@ -1,21 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/di.dart';
import '../../../core/session/session_cubit.dart';
import '../cubit/login_cubit.dart';
import '../cubit/login_state.dart';
import 'agreement_view.dart';
import 'otp_view.dart';
import 'permission_view.dart';
import 'phone_view.dart';
/// مضيف تدفّق الدخول: خطوة واحدة معروضة في كل لحظة، والانتقال بينها من
@@ -1,26 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/design/tokens.dart';
import '../../../core/design/tripz_colors.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/ui/failure_text.dart';
import '../../../core/ui/tripz_button.dart';
import '../../../core/ui/tripz_otp_field.dart';
import '../../../core/ui/tripz_scaffold.dart';
import '../cubit/login_cubit.dart';
import '../cubit/login_state.dart';
import '../../../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 {
const OtpView({super.key});
@@ -1,22 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/design/tokens.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/ui/status_banner.dart';
import '../../../core/ui/tripz_button.dart';
import '../../../core/ui/tripz_scaffold.dart';
import '../cubit/login_cubit.dart';
import '../cubit/login_state.dart';
import '../../../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` بمراقبة دورة الحياة.
@@ -1,28 +1,48 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/design/tokens.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/tenant/tenant_cubit.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_scaffold.dart';
import '../../../core/ui/tripz_text_field.dart';
import '../cubit/login_cubit.dart';
import '../cubit/login_state.dart';
import '../../../core/ui/form_header.dart';
class PhoneView extends StatelessWidget {
class PhoneView extends StatefulWidget {
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
Widget build(BuildContext context) {
final l10n = context.l10n;
@@ -30,6 +50,7 @@ class PhoneView extends StatelessWidget {
buildWhen: (p, n) =>
p.status != n.status ||
p.phoneError != n.phoneError ||
p.country != n.country ||
p.failure != n.failure,
builder: (context, state) {
final cubit = context.read<LoginCubit>();
@@ -49,11 +70,13 @@ class PhoneView extends StatelessWidget {
lead: l10n.phoneLead,
),
const SizedBox(height: Space.lg),
TripzTextField.phone(
PhoneField(
controller: _phone,
country: state.country,
onCountryChanged: cubit.countryChanged,
label: l10n.phoneLabel,
hint: l10n.phoneHint,
enabled: !state.isSubmitting,
onChanged: cubit.phoneChanged,
onSubmitted: (_) => cubit.submitPhone(),
errorText: state.phoneError == null
? null
@@ -1,13 +1,9 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/api/api_exception.dart';
import '../../../core/api/api_failure.dart';
import '../../../core/session/app_user.dart';
import '../../../core/ui/view_status.dart';
import '../data/profile_repository.dart';
class ProfileState {
@@ -1,23 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/design/tokens.dart';
import '../../../core/design/tripz_colors.dart';
import '../../../core/di.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/session/session_cubit.dart';
import '../../../core/ui/tripz_button.dart';
import '../../../core/ui/tripz_scaffold.dart';
import '../../../core/ui/tripz_text_field.dart';
import '../cubit/profile_cubit.dart';
/// تعديل الاسم. **الرقم لا يُعدَّل**: هو هوية الحساب على الخادم، وتغييره
@@ -1,25 +1,15 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/design/tokens.dart';
import '../../../core/di.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/session/session_cubit.dart';
import '../../../core/ui/failure_text.dart';
import '../../../core/ui/tripz_button.dart';
import '../../../core/ui/tripz_scaffold.dart';
import '../../../core/ui/tripz_text_field.dart';
import '../../../core/ui/form_header.dart';
import '../../../core/ui/tripz_button.dart';
import '../../../core/ui/tripz_scaffold.dart';
import '../../../core/ui/tripz_text_field.dart';
import '../cubit/profile_cubit.dart';
class ProfileSetupPage extends StatefulWidget {
@@ -1,9 +1,7 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/api/api_exception.dart';
import '../data/rating_repository.dart';
import 'rating_state.dart';
class RatingCubit extends Cubit<RatingState> {
@@ -1,27 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/design/tokens.dart';
import '../../../core/design/tripz_colors.dart';
import '../../../core/di.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/ui/tripz_button.dart';
import '../../../core/ui/tripz_scaffold.dart';
import '../../../core/ui/tripz_text_field.dart';
import '../cubit/rating_cubit.dart';
import '../cubit/rating_state.dart';
import '../data/rating_repository.dart';
import 'star_row.dart';
/// شاشة التقييم — **تُفرض عند فتح التطبيق** إن كانت هناك رحلة معلّقة
@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import '../../../core/design/tokens.dart';
import '../../../core/design/tripz_colors.dart';
/// خمس نجوم — أهداف لمس ≥ 48 لأن الشاشة تُستعمل عادةً والمستخدم مستعجل.
@@ -1,7 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'settings_state.dart';
@@ -1,5 +1,4 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
/// المظهر واللغة (docs/26 §4) — محفوظان محلياً، وليسا سرّاً فيذهبان إلى
@@ -1,17 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/design/tokens.dart';
import '../../../core/design/tripz_colors.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/ui/tripz_scaffold.dart';
import '../cubit/settings_cubit.dart';
import '../cubit/settings_state.dart';
/// المظهر واللغة. تبديل اللغة **يقلب الاتجاه كاملاً** بلا إعادة تشغيل —
@@ -1,19 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/build_config.dart';
import '../../../core/config.dart';
import '../../../core/design/tokens.dart';
import '../../../core/design/tripz_colors.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/session/session_cubit.dart';
import '../../../core/tenant/tenant_cubit.dart';
/// شاشة الإقلاع: تستعيد الجلسة ثم يتولّى الراوتر التوجيه. لا تنقّل من هنا —
@@ -1,15 +1,10 @@
import 'package:flutter/material.dart';
import '../../../core/build_config.dart';
import '../../../core/design/tokens.dart';
import '../../../core/design/tripz_colors.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/ui/tripz_card.dart';
import '../../../core/ui/tripz_scaffold.dart';
/// المساعدة و«عن التطبيق». محتوى محلّي عمداً: الخادم لا يوفّر نقطة أسئلة
@@ -1,11 +1,8 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/api/api_exception.dart';
import '../../../core/ui/view_status.dart';
import '../data/history_repository.dart';
import '../data/models/trip.dart';
class HistoryState {
@@ -1,5 +1,4 @@
import '../../../core/api/api_client.dart';
import 'models/trip.dart';
/// سجلّ الرحلات. لا يستورد ميزة الرحلة كمنطق — يعيد استعمال نموذجها فقط،
@@ -1,9 +1,6 @@
import '../../../core/api/antlaq_api.dart';
import 'models/geo_point.dart';
import 'models/place.dart';
import 'models/route_info.dart';
/// كل الخرائط من map-saas مباشرة (قرار 2026-07-20). نقاط `/maps/*` في باك إند
@@ -1,11 +1,7 @@
import '../../../core/api/api_client.dart';
import 'models/fare_quote.dart';
import 'models/geo_point.dart';
import 'models/ride_type.dart';
import 'models/trip.dart';
class TripRepository {
@@ -1,27 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/design/tokens.dart';
import '../../../core/design/tripz_colors.dart';
import '../../../core/design/typography.dart';
import '../../../core/di.dart';
import '../../../core/format/money.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/ui/empty_view.dart';
import '../../../core/ui/tripz_card.dart';
import '../../../core/ui/tripz_scaffold.dart';
import '../cubit/history_cubit.dart';
import '../data/models/trip.dart';
class HistoryPage extends StatefulWidget {
@@ -1,11 +1,8 @@
import 'package:flutter/material.dart';
import '../../../../core/design/tokens.dart';
import '../../../../core/design/tripz_colors.dart';
import '../../../../core/design/typography.dart';
import '../../data/models/trip.dart';
/// بطاقة السائق ولوحة المركبة — أهم ما يبحث عنه الراكب في الشارع، فيأخذ
@@ -1,15 +1,10 @@
import 'package:flutter/material.dart';
import '../../../../core/design/tokens.dart';
import '../../../../core/design/tripz_colors.dart';
import '../../../../core/design/typography.dart';
import '../../../../core/format/money.dart';
import '../../../../core/l10n/l10n.dart';
import '../../data/models/trip.dart';
/// ملخّص الأجرة عند الإنهاء.
@@ -1,11 +1,8 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/api/api_exception.dart';
import '../../../core/ui/view_status.dart';
import '../data/wallet_repository.dart';
import 'wallet_state.dart';
class WalletCubit extends Cubit<WalletState> {
@@ -1,9 +1,7 @@
import 'package:equatable/equatable.dart';
import '../../../core/ui/view_status.dart';
import '../data/models/wallet.dart';
import '../data/models/wallet_txn.dart';
enum WalletAction { none, toppingUp, topupFailed, topupDone }
@@ -1,7 +1,5 @@
import '../../../core/api/api_client.dart';
import 'models/wallet.dart';
import 'models/wallet_txn.dart';
class WalletRepository {
@@ -1,11 +1,8 @@
import 'package:flutter/material.dart';
import '../../../../core/design/tokens.dart';
import '../../../../core/design/tripz_colors.dart';
import '../../../../core/design/typography.dart';
import '../../../../core/format/money.dart';
/// بطاقة الرصيد — الرقم هو بطل الشاشة، فيأخذ أكبر حجم وأعلى تباين.
@@ -1,13 +1,9 @@
import 'package:flutter/material.dart';
import '../../../../core/design/tokens.dart';
import '../../../../core/design/tripz_colors.dart';
import '../../../../core/design/typography.dart';
import '../../../../core/format/money.dart';
import '../../data/models/wallet_txn.dart';
class TxnTile extends StatelessWidget {
-3
View File
@@ -1,11 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'app.dart';
import 'core/di.dart';
import 'core/session/session_cubit.dart';
Future<void> main() async {
+1 -1
View File
@@ -12,7 +12,7 @@ cd "$(dirname "$0")/.."
# ملفات يملكها السائق ولا تُنسخ من الراكب أبداً
OWN=(
"core/config.dart" # appRole = driver
"core/build_config.dart" # اسم التطبيق
"core/build_config.dart" # اسم التطبيق (الألوان تُحدَّث يدوياً معاً)
"core/di.dart" # DutyCubit + OnboardingCubit + PayoutCubit
"core/router.dart" # DutyPage + EarningsPage + بوابة التسجيل
"core/l10n/app_ar.arb"
-9
View File
@@ -1,23 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'core/build_config.dart';
import 'core/design/theme.dart';
import 'core/di.dart';
import 'core/l10n/l10n.dart';
import 'core/router.dart';
import 'core/tenant/tenant_cubit.dart';
import 'features/settings/cubit/settings_cubit.dart';
import 'features/settings/cubit/settings_state.dart';
class TripzApp extends StatelessWidget {
@@ -1,7 +1,6 @@
import 'package:dio/dio.dart';
import '../config.dart';
import 'api_exception.dart';
/// عميل map-saas — **منفصل تماماً** عن `ApiClient`.
@@ -1,11 +1,8 @@
import 'package:dio/dio.dart';
import '../config.dart';
import '../storage/token_store.dart';
import 'api_exception.dart';
import 'auth_interceptor.dart';
/// الواجهة الوحيدة للخادم. لا تُنشأ `Dio` في أي مكان آخر.
@@ -3,9 +3,7 @@ import 'dart:async';
import 'package:dio/dio.dart';
import '../build_config.dart';
import '../config.dart';
import '../storage/token_store.dart';
/// يثبّت ترويسات كل طلب، ويجدّد التوكن **استباقياً** قبل انتهائه.
+7 -1
View File
@@ -12,7 +12,13 @@ class BuildConfig {
static const String tenantSlug = 'siro';
/// اللون البذرة — منه تُشتقّ `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 splashLogoAsset = 'assets/images/splash_logo.png';
+23 -3
View File
@@ -1,11 +1,8 @@
import 'package:flutter/material.dart';
import '../build_config.dart';
import 'tokens.dart';
import 'tripz_colors.dart';
import 'typography.dart';
/// دالة بناء الثيم الوحيدة (docs/26 §4): مدخلان لا ثالث لهما.
@@ -13,9 +10,32 @@ import 'typography.dart';
/// الاتجاه (RTL/LTR) **لا يُفرض هنا** — `MaterialApp` يشتقّه من الـlocale
/// وحده. فرض `Directionality.rtl` يكسر الإنجليزية (docs/26 §4).
ThemeData buildTheme(Brightness brightness, Locale locale) {
// البذرة الكحلية داكنة جداً، و`fromSeed` وحدها تنتج منها أسطحاً باهتة.
// نثبّت الأساسي والثانوي صراحةً ونترك للمشتقّ باقي الأدوار — فيبقى الشكل
// قريباً من سيرو مع ضمان التباين الذي يوفّره M3.
final scheme = ColorScheme.fromSeed(
seedColor: BuildConfig.seedColor,
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 texts = buildTextTheme(brightness, locale);
@@ -40,35 +40,37 @@ class TripzColors extends ThemeExtension<TripzColors> {
/// لون هيكل التحميل (skeleton) — لا spinner فارغ (docs/26 §8.4).
final Color skeleton;
// الأرقام من نظام سيرو الميداني: نجاح `#22C55E` · تحذير `#F59E0B` ·
// خطأ `#EF4444` — مألوفة لمستخدمي سيرو ومجرَّبة على شاشات الشمس.
static const light = TripzColors(
success: Color(0xFF1B8A5A),
success: Color(0xFF22C55E),
onSuccess: Color(0xFFFFFFFF),
warning: Color(0xFFB4690E),
onWarning: Color(0xFFFFFFFF),
danger: Color(0xFFC5303B),
warning: Color(0xFFF59E0B),
onWarning: Color(0xFF2A1A00),
danger: Color(0xFFEF4444),
onDanger: Color(0xFFFFFFFF),
info: Color(0xFF2563A8),
info: Color(0xFF8C9CF8),
surfaceRaised: Color(0xFFFFFFFF),
mapRoute: Color(0xFF1F6FEB),
mapOrigin: Color(0xFF1B8A5A),
mapDestination: Color(0xFFC5303B),
skeleton: Color(0xFFE6E8EC),
mapRoute: Color(0xFF1A2340),
mapOrigin: Color(0xFF22C55E),
mapDestination: Color(0xFFEF4444),
skeleton: Color(0xFFE2E8F0),
);
static const dark = TripzColors(
success: Color(0xFF4ECB8D),
success: Color(0xFF4ADE80),
onSuccess: Color(0xFF00281A),
warning: Color(0xFFE9A63B),
warning: Color(0xFFFBBF24),
onWarning: Color(0xFF2A1A00),
danger: Color(0xFFF2707A),
danger: Color(0xFFF87171),
onDanger: Color(0xFF33000A),
info: Color(0xFF7FB2F0),
// ليس أسود صرفاً — رمادي داكن متدرّج.
surfaceRaised: Color(0xFF20232A),
mapRoute: Color(0xFF5B9BFF),
mapOrigin: Color(0xFF4ECB8D),
mapDestination: Color(0xFFF2707A),
skeleton: Color(0xFF2C3038),
info: Color(0xFF8C9CF8),
// ليس أسود صرفاً — رمادي داكن متدرّج (بطاقة سيرو `#2C2C2E`).
surfaceRaised: Color(0xFF2C2C2E),
mapRoute: Color(0xFF8C9CF8),
mapOrigin: Color(0xFF4ADE80),
mapDestination: Color(0xFFF87171),
skeleton: Color(0xFF3A3A3C),
);
@override
-24
View File
@@ -1,57 +1,33 @@
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get_it/get_it.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../features/auth/cubit/login_cubit.dart';
import '../features/auth/data/auth_repository.dart';
import '../features/chat/cubit/chat_cubit.dart';
import '../features/chat/data/chat_repository.dart';
import '../features/profile/data/profile_repository.dart';
import '../features/rating/cubit/rating_cubit.dart';
import '../features/rating/data/rating_repository.dart';
import '../features/settings/cubit/settings_cubit.dart';
import '../features/trip/cubit/history_cubit.dart';
import '../features/trip/cubit/ride_cubit.dart';
import '../features/trip/data/history_repository.dart';
import '../features/trip/data/maps_repository.dart';
import '../features/trip/data/trip_repository.dart';
import '../features/wallet/cubit/wallet_cubit.dart';
import '../features/wallet/data/wallet_repository.dart';
import 'api/antlaq_api.dart';
import 'api/api_client.dart';
import 'build_config.dart';
import 'location/location_service.dart';
import 'notifications/push_service.dart';
import 'realtime/realtime_service.dart';
import 'session/session_cubit.dart';
import 'session/session_source.dart';
import 'storage/token_store.dart';
import 'tenant/tenant_cubit.dart';
final sl = GetIt.instance;
@@ -1,11 +1,8 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import '../api/api_client.dart';
import '../api/api_exception.dart';
/// إشعارات FCM — **الطبقة الوحيدة التي تعرف Firebase**.
@@ -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', 'إم تي إن كاش'),
shamCash('sham_cash', 'Sham Cash', 'شام كاش'),
paymob('paymob', 'PayMob', 'باي موب'),
zainCash('zain_cash', 'ZainCash', 'زين كاش'),
qiCard('qi_card', 'Qi Card', 'كي كارد'),
cash('cash', 'Cash', 'نقداً');
const PaymentChannel(this.wire, this.nameEn, this.nameAr);
@@ -38,6 +40,14 @@ enum PaymentChannel {
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],
};
-14
View File
@@ -1,36 +1,22 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
import '../features/auth/view/login_page.dart';
import '../features/chat/view/chat_page.dart';
import '../features/profile/view/profile_edit_page.dart';
import '../features/profile/view/profile_setup_page.dart';
import '../features/rating/data/rating_repository.dart';
import '../features/rating/view/rating_page.dart';
import '../features/settings/view/settings_page.dart';
import '../features/splash/view/splash_page.dart';
import '../features/support/view/support_page.dart';
import '../features/trip/view/history_page.dart';
import '../features/trip/view/ride_page.dart';
import '../features/wallet/view/wallet_page.dart';
import 'di.dart';
import 'session/session_cubit.dart';
import 'session/session_state.dart';
class Routes {
@@ -3,11 +3,8 @@ import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../api/api_exception.dart';
import 'app_user.dart';
import 'session_source.dart';
import 'session_state.dart';
/// حالة الجلسة على مستوى التطبيق — مصدر قرار التوجيه الوحيد
@@ -1,5 +1,4 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'tenant_cubit.dart';
@@ -1,13 +1,9 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../api/api_client.dart';
import '../api/api_exception.dart';
import '../build_config.dart';
import 'tenant_config.dart';
/// يجلب إعداد المستأجر مرة عند الإقلاع، ويخزّنه محلياً.
@@ -1,25 +1,15 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../build_config.dart';
import '../config.dart';
import '../design/tokens.dart';
import '../design/tripz_colors.dart';
import '../l10n/l10n.dart';
import '../router.dart';
import '../session/session_cubit.dart';
import '../session/session_state.dart';
import '../tenant/feature_gate.dart';
/// الدراور — نقطة تطبيق **طبقتَي الميزات** معاً (docs/23 §5 + docs/38 §10):
@@ -1,11 +1,8 @@
import 'package:flutter/material.dart';
import '../design/tokens.dart';
import '../design/tripz_colors.dart';
import '../l10n/l10n.dart';
import 'tripz_button.dart';
/// حالة الفراغ: رسالة **تدلّ على الفعل التالي** لا مجرّد «لا يوجد»
@@ -1,11 +1,8 @@
import 'package:flutter/material.dart';
import '../design/tokens.dart';
import '../design/tripz_colors.dart';
import '../l10n/l10n.dart';
import 'tripz_button.dart';
/// حالة الخطأ: رسالة **بالعربية** مع «أعد المحاولة» — لا شاشة صامتة
@@ -1,13 +1,9 @@
import 'package:flutter/material.dart';
import '../../features/auth/cubit/phone_error.dart';
import '../api/api_failure.dart';
import '../design/tokens.dart';
import '../design/tripz_colors.dart';
import '../l10n/l10n.dart';
/// النقطة **الوحيدة** التي تُترجَم فيها رموز الفشل إلى نصّ.
@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import '../design/tokens.dart';
import '../design/tripz_colors.dart';
/// ترويسة موحّدة لشاشات الدخول: أيقونة ثم عنوان ثم سطر شارح.
+141
View File
@@ -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
View File
@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import '../design/tokens.dart';
import '../design/tripz_colors.dart';
/// هيكل تحميل — **لا spinner فارغ** (docs/26 §8.4).
@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import '../design/tokens.dart';
import '../design/tripz_colors.dart';
enum BannerTone { info, success, warning, danger }
@@ -1,9 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../design/tokens.dart';
import '../design/typography.dart';
/// حقل رمز التحقّق — أربع خانات (طول رمز تريبز، docs/38 §2).
@@ -1,13 +1,9 @@
import 'package:flutter/material.dart';
import '../design/tokens.dart';
import 'empty_view.dart';
import 'error_view.dart';
import 'skeleton.dart';
import 'view_status.dart';
/// نقطة التنظيم المركزية (docs/26 §6). **ممنوع `Scaffold` خام في
@@ -1,9 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../design/tokens.dart';
import '../design/typography.dart';
/// حقل الإدخال الموحّد (docs/26 §7): نصّ · هاتف · بحث.
@@ -1,22 +1,16 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:geolocator/geolocator.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../core/api/api_exception.dart';
import '../../../core/api/api_failure.dart';
import '../../../core/config.dart';
import '../../../core/payments/country_codes.dart';
import '../data/auth_repository.dart';
import 'login_state.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) {
emit(state.copyWith(
phone: value,
@@ -130,14 +133,14 @@ class LoginCubit extends Cubit<LoginState> {
}
Future<void> submitPhone() async {
final error = validatePhone(state.phone);
final error = validatePhone(state.phone, state.country);
if (error != null) {
emit(state.copyWith(phoneError: error));
return;
}
emit(state.copyWith(status: LoginStatus.submitting, clearFailure: true));
try {
await _repo.sendOtp(state.phone.trim());
await _repo.sendOtp(state.e164);
emit(state.copyWith(status: LoginStatus.idle, step: LoginStep.otp));
_startResendCountdown();
} on ApiException catch (e) {
@@ -164,7 +167,7 @@ class LoginCubit extends Cubit<LoginState> {
if (!state.canResend) return;
emit(state.copyWith(status: LoginStatus.submitting, clearFailure: true));
try {
await _repo.sendOtp(state.phone.trim());
await _repo.sendOtp(state.e164);
emit(state.copyWith(status: LoginStatus.idle));
_startResendCountdown();
} on ApiException catch (e) {
@@ -182,7 +185,7 @@ class LoginCubit extends Cubit<LoginState> {
if (code.length != AppConfig.otpLength) return;
emit(state.copyWith(status: LoginStatus.submitting, clearFailure: true));
try {
final user = await _repo.verifyOtp(phone: state.phone.trim(), code: code);
final user = await _repo.verifyOtp(phone: state.e164, code: code);
_resendTimer?.cancel();
emit(state.copyWith(
status: LoginStatus.success,
@@ -1,9 +1,8 @@
import 'package:equatable/equatable.dart';
import '../../../core/api/api_failure.dart';
import '../../../core/payments/country_codes.dart';
import '../../../core/session/app_user.dart';
import 'phone_error.dart';
/// خطوات الدخول بالترتيب الميداني المثبت في سيرو (docs/39 §2): بوابتان
@@ -17,6 +16,7 @@ class LoginState extends Equatable {
this.step = LoginStep.agreement,
this.status = LoginStatus.idle,
this.phone = '',
this.country = CountryCode.jordan,
this.agreed = false,
this.failure,
this.phoneError,
@@ -31,6 +31,13 @@ class LoginState extends Equatable {
/// الرقم كما أدخله المستخدم — الخادم يطبّعه، فلا نطبّعه نحن (docs/38 §2).
final String phone;
/// الدولة المختارة — تحدّد المفتاح الدولي وطول التحقّق. تُبدأ من
/// `countryPack` المستأجر لا من لغة الجهاز.
final CountryCode country;
/// ما يُرسل للخادم: المفتاح + الرقم بلا `+` (`962790000001`).
String get e164 => '${country.dial}${phone.trim()}';
final bool agreed;
final ApiFailure? failure;
final PhoneError? phoneError;
@@ -51,6 +58,7 @@ class LoginState extends Equatable {
LoginStep? step,
LoginStatus? status,
String? phone,
CountryCode? country,
bool? agreed,
ApiFailure? failure,
PhoneError? phoneError,
@@ -64,6 +72,7 @@ class LoginState extends Equatable {
step: step ?? this.step,
status: status ?? this.status,
phone: phone ?? this.phone,
country: country ?? this.country,
agreed: agreed ?? this.agreed,
failure: clearFailure ? null : (failure ?? this.failure),
phoneError: clearPhoneError ? null : (phoneError ?? this.phoneError),
@@ -79,6 +88,7 @@ class LoginState extends Equatable {
step,
status,
phone,
country,
agreed,
failure,
phoneError,
@@ -1,11 +1,15 @@
import '../../../core/payments/country_codes.dart';
/// أخطاء تحقّق **محليّة** لرقم الهاتف — تُكتشف قبل أي نداء شبكة.
/// الشروط الثلاثة منقولة من التحقّق المطبَّق ميدانياً في سيرو (docs/39 §2).
enum PhoneError { empty, leadingZero, tooShort }
PhoneError? validatePhone(String raw) {
/// الطول المتوقّع من **الدولة المختارة** لا رقماً ثابتاً: الأردن وسوريا 9
/// خانات، ومصر والعراق 10.
PhoneError? validatePhone(String raw, CountryCode country) {
final v = raw.trim();
if (v.isEmpty) return PhoneError.empty;
if (v.startsWith('0')) return PhoneError.leadingZero;
if (v.length < 9) return PhoneError.tooShort;
if (v.length < country.nationalLength) return PhoneError.tooShort;
return null;
}
@@ -1,9 +1,6 @@
import '../../../core/api/api_client.dart';
import '../../../core/session/app_user.dart';
import '../../../core/session/session_source.dart';
import '../../../core/storage/token_store.dart';
/// كل نداءات المصادقة (docs/38 §2). الـCubit لا ينادي Dio مباشرة أبداً
@@ -1,22 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/design/tokens.dart';
import '../../../core/design/tripz_colors.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/ui/tripz_button.dart';
import '../../../core/ui/tripz_scaffold.dart';
import '../cubit/login_cubit.dart';
import '../cubit/login_state.dart';
import '../../../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). تُحفظ الموافقة فلا تتكرر.
@@ -1,21 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/di.dart';
import '../../../core/session/session_cubit.dart';
import '../cubit/login_cubit.dart';
import '../cubit/login_state.dart';
import 'agreement_view.dart';
import 'otp_view.dart';
import 'permission_view.dart';
import 'phone_view.dart';
/// مضيف تدفّق الدخول: خطوة واحدة معروضة في كل لحظة، والانتقال بينها من
@@ -1,26 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/design/tokens.dart';
import '../../../core/design/tripz_colors.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/ui/failure_text.dart';
import '../../../core/ui/tripz_button.dart';
import '../../../core/ui/tripz_otp_field.dart';
import '../../../core/ui/tripz_scaffold.dart';
import '../cubit/login_cubit.dart';
import '../cubit/login_state.dart';
import '../../../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 {
const OtpView({super.key});
@@ -1,22 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/design/tokens.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/ui/status_banner.dart';
import '../../../core/ui/tripz_button.dart';
import '../../../core/ui/tripz_scaffold.dart';
import '../cubit/login_cubit.dart';
import '../cubit/login_state.dart';
import '../../../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` بمراقبة دورة الحياة.
@@ -1,28 +1,48 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/design/tokens.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/tenant/tenant_cubit.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_scaffold.dart';
import '../../../core/ui/tripz_text_field.dart';
import '../cubit/login_cubit.dart';
import '../cubit/login_state.dart';
import '../../../core/ui/form_header.dart';
class PhoneView extends StatelessWidget {
class PhoneView extends StatefulWidget {
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
Widget build(BuildContext context) {
final l10n = context.l10n;
@@ -30,6 +50,7 @@ class PhoneView extends StatelessWidget {
buildWhen: (p, n) =>
p.status != n.status ||
p.phoneError != n.phoneError ||
p.country != n.country ||
p.failure != n.failure,
builder: (context, state) {
final cubit = context.read<LoginCubit>();
@@ -49,11 +70,13 @@ class PhoneView extends StatelessWidget {
lead: l10n.phoneLead,
),
const SizedBox(height: Space.lg),
TripzTextField.phone(
PhoneField(
controller: _phone,
country: state.country,
onCountryChanged: cubit.countryChanged,
label: l10n.phoneLabel,
hint: l10n.phoneHint,
enabled: !state.isSubmitting,
onChanged: cubit.phoneChanged,
onSubmitted: (_) => cubit.submitPhone(),
errorText: state.phoneError == null
? null
@@ -1,13 +1,9 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/api/api_exception.dart';
import '../../../core/api/api_failure.dart';
import '../../../core/session/app_user.dart';
import '../../../core/ui/view_status.dart';
import '../data/profile_repository.dart';
class ProfileState {
@@ -1,23 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/design/tokens.dart';
import '../../../core/design/tripz_colors.dart';
import '../../../core/di.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/session/session_cubit.dart';
import '../../../core/ui/tripz_button.dart';
import '../../../core/ui/tripz_scaffold.dart';
import '../../../core/ui/tripz_text_field.dart';
import '../cubit/profile_cubit.dart';
/// تعديل الاسم. **الرقم لا يُعدَّل**: هو هوية الحساب على الخادم، وتغييره
@@ -1,25 +1,15 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/design/tokens.dart';
import '../../../core/di.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/session/session_cubit.dart';
import '../../../core/ui/failure_text.dart';
import '../../../core/ui/tripz_button.dart';
import '../../../core/ui/tripz_scaffold.dart';
import '../../../core/ui/tripz_text_field.dart';
import '../../../core/ui/form_header.dart';
import '../../../core/ui/tripz_button.dart';
import '../../../core/ui/tripz_scaffold.dart';
import '../../../core/ui/tripz_text_field.dart';
import '../cubit/profile_cubit.dart';
class ProfileSetupPage extends StatefulWidget {
@@ -1,9 +1,7 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/api/api_exception.dart';
import '../data/rating_repository.dart';
import 'rating_state.dart';
class RatingCubit extends Cubit<RatingState> {
@@ -1,27 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/design/tokens.dart';
import '../../../core/design/tripz_colors.dart';
import '../../../core/di.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/ui/tripz_button.dart';
import '../../../core/ui/tripz_scaffold.dart';
import '../../../core/ui/tripz_text_field.dart';
import '../cubit/rating_cubit.dart';
import '../cubit/rating_state.dart';
import '../data/rating_repository.dart';
import 'star_row.dart';
/// شاشة التقييم — **تُفرض عند فتح التطبيق** إن كانت هناك رحلة معلّقة
@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import '../../../core/design/tokens.dart';
import '../../../core/design/tripz_colors.dart';
/// خمس نجوم — أهداف لمس ≥ 48 لأن الشاشة تُستعمل عادةً والمستخدم مستعجل.
@@ -1,7 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'settings_state.dart';

Some files were not shown because too many files have changed in this diff Show More