fix(apps): إصلاحات من أول تجربة حقيقية على الجهاز
## تعذّر حساب الأجرة — الخطأ في التطبيق لا في جدول الكازان الخادم يبذر التعرفة بـ city: 'default' (tenants.service.ts:156) ويستعمل dto.city ?? 'default' عند إنشاء الرحلة، وكنت أرسل city: '' فيرجّع 404 «No active tariff for /economy». تحقّق حيّ: city='default' → total 1.55 JOD ✅ city='' → 404 ❌ city='amman' → 404 ❌ لا تغيير مطلوب على السيرفر. أُضيف AppConfig.defaultCity موثّقاً بالسبب. ## التسعير مرة واحدة لكل الأنواع كان نداء شبكة عند كل ضغطة نوع. صار _priceAll يسعّر كل الأنواع بالتوازي بعد اكتمال المسار — المسافة والمدة ثابتتان والفرق في التعرفة وحدها. الراكب يرى سعر الاقتصادي والمريح معاً تحت كل خيار ويقارن بنظرة، وتبديل النوع بلا نداء. ## الكاميرا لا تتبع الموقع سببان: الموقع يصل قبل إنشاء الخريطة فيضيع التحريك، ولا فحص للإذن/الخدمة قبل getCurrentPosition فيُبتلع الخطأ صامتاً. - _onCreated يلحق بما فات عند جهوز الخريطة - LocationService.ensureReady يفحص الخدمة والإذن أولاً - didChangeAppLifecycleState يعيد المحاولة عند العودة من إعدادات النظام - حالة noLocation تُعرض للمستخدم مع زر إعادة بدل خريطة صامتة ## الخريطة والتنقّل - إطار المسار من حدوده الفعلية مع حشوة سفلية بمقدار ما تغطّيه الورقة - علامتا البداية والنهاية دائرتان ملوّنتان تطابقان ألوان الخط الزمني - زر رجوع في ورقة التأكيد + زر إعادة حساب عند فشل التسعيرة: كان المستخدم يعلق في الشاشة بلا مخرج ## المحفظة — على أي أساس يُشحن POST /wallet/topup شحنٌ تجريبي بلا مزوّد دفع خلفه (الخادم نفسه يصفه كذلك). أُضيف PaymentChannel مشتقّاً من countryPack المستأجر لا من لغة الجهاز: jo → CliQ · sy → سيرياتيل/MTN/شام كاش · eg → PayMob. ويُعرض تنبيه صريح بأن الشحن بلا تحصيل فعلي — إخفاؤه يجعل المستخدم يظنّ أنه دفع. وسحب السائق صار يستعمل القناة المختارة بدل 'cliq' المكتوبة في الكود. flutter analyze نظيف في التطبيقين. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
29f375b122
commit
8976821a93
@@ -14,6 +14,7 @@ import '../data/maps_repository.dart';
|
||||
|
||||
import '../data/models/geo_point.dart';
|
||||
|
||||
import '../data/models/fare_quote.dart';
|
||||
import '../data/models/place.dart';
|
||||
|
||||
import '../data/models/ride_type.dart';
|
||||
@@ -67,16 +68,28 @@ class RideCubit extends Cubit<RideState> {
|
||||
|
||||
// الكاميرا تتوسّط الموقع **فوراً**، ولا تنتظر العنوان — انتظارها كان
|
||||
// باگ سيرو المُصلَح 2026-07-20.
|
||||
unawaited(_locateMe());
|
||||
unawaited(locateMe());
|
||||
unawaited(_loadRideTypes());
|
||||
unawaited(_resumeLiveTrip());
|
||||
}
|
||||
|
||||
Future<void> _locateMe() async {
|
||||
/// تُنادى عند الإقلاع وعند العودة للتطبيق — الإذن قد يُمنح من إعدادات
|
||||
/// النظام بينما التطبيق في الخلفية.
|
||||
Future<void> locateMe() async {
|
||||
final pos = await _location.current();
|
||||
if (pos == null || isClosed) return;
|
||||
if (pos == null || isClosed) {
|
||||
// بلا موقع لا خريطة مفيدة — تُعرض الحالة كي يفهم المستخدم السبب.
|
||||
if (!isClosed && state.origin == null) {
|
||||
emit(state.copyWith(error: RideError.noLocation));
|
||||
}
|
||||
return;
|
||||
}
|
||||
final point = GeoPoint(pos.lat, pos.lng);
|
||||
emit(state.copyWith(origin: point, cameraTarget: point));
|
||||
emit(state.copyWith(
|
||||
origin: point,
|
||||
cameraTarget: point,
|
||||
error: state.error == RideError.noLocation ? RideError.none : null,
|
||||
));
|
||||
|
||||
final place = await _maps.reverse(point);
|
||||
if (isClosed || place == null) return;
|
||||
@@ -91,6 +104,8 @@ class RideCubit extends Cubit<RideState> {
|
||||
rideTypes: types,
|
||||
selectedRideType: state.selectedRideType ?? types.first,
|
||||
));
|
||||
// قد تصل الأنواع بعد رسم المسار — عندها تُسعَّر فوراً بلا تدخّل.
|
||||
if (state.route != null && state.quotes.isEmpty) await _priceAll();
|
||||
} on ApiException {
|
||||
// قائمة الأنواع ليست حاجزاً للخريطة — تُعاد المحاولة عند التأكيد.
|
||||
}
|
||||
@@ -115,9 +130,12 @@ class RideCubit extends Cubit<RideState> {
|
||||
|
||||
void collapsePlanner() => emit(state.copyWith(phase: RidePhase.idle));
|
||||
|
||||
/// تبديل النوع **لا يُطلق نداءً**: كل الأنواع مسعَّرة مسبقاً في [_priceAll].
|
||||
void selectRideType(RideType type) {
|
||||
emit(state.copyWith(selectedRideType: type));
|
||||
unawaited(_priceIt());
|
||||
emit(state.copyWith(
|
||||
selectedRideType: type,
|
||||
quote: state.quoteFor(type.code),
|
||||
));
|
||||
}
|
||||
|
||||
/// بحث نصّي عن مكان، بمهلة ارتداد: كل ضغطة زر لا تُطلق نداءً.
|
||||
@@ -186,6 +204,21 @@ class RideCubit extends Cubit<RideState> {
|
||||
emit(state.copyWith(stops: next));
|
||||
}
|
||||
|
||||
/// تراجع خطوة واحدة — المخرج الوحيد من شاشة التأكيد حين تفشل التسعيرة.
|
||||
void back() {
|
||||
switch (state.phase) {
|
||||
case RidePhase.confirming:
|
||||
emit(state.copyWith(phase: RidePhase.planning, error: RideError.none));
|
||||
case RidePhase.pickingOnMap:
|
||||
case RidePhase.planning:
|
||||
emit(state.copyWith(phase: RidePhase.idle, error: RideError.none));
|
||||
case RidePhase.idle:
|
||||
case RidePhase.searching:
|
||||
case RidePhase.active:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void clearDestination() => emit(state.copyWith(
|
||||
clearDestination: true,
|
||||
clearRoute: true,
|
||||
@@ -204,7 +237,7 @@ class RideCubit extends Cubit<RideState> {
|
||||
final route = await _maps.route(from, to);
|
||||
if (isClosed) return;
|
||||
emit(state.copyWith(route: route, phase: RidePhase.confirming));
|
||||
await _priceIt();
|
||||
await _priceAll();
|
||||
} on ApiException {
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(busy: false, error: RideError.routeFailed));
|
||||
@@ -212,32 +245,52 @@ class RideCubit extends Cubit<RideState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// التعرفة تحتاج مسافة ومدة محسوبتين — لا إحداثيات (docs/38 §7).
|
||||
Future<void> _priceIt() async {
|
||||
/// يسعّر **كل الأنواع دفعةً واحدة** بعد اكتمال المسار.
|
||||
///
|
||||
/// المسافة والمدة ثابتتان لكل الأنواع، فالفرق في التعرفة وحدها. النداءات
|
||||
/// متوازية ومرة واحدة — لا نداء عند كل ضغطة نوع.
|
||||
///
|
||||
/// التعرفة تحتاج مسافة ومدة محسوبتين لا إحداثيات، و`city` يجب أن تكون
|
||||
/// `default` كما يبذرها الخادم — أي قيمة أخرى ترجّع 404 (docs/38 §7).
|
||||
/// إعادة تسعير يدوية — مخرج المستخدم حين يفشل الحساب.
|
||||
Future<void> repriceAll() => _priceAll();
|
||||
|
||||
Future<void> _priceAll() async {
|
||||
final route = state.route;
|
||||
final type = state.selectedRideType;
|
||||
if (route == null || type == null) return;
|
||||
final types = state.rideTypes;
|
||||
if (route == null || types.isEmpty) return;
|
||||
|
||||
emit(state.copyWith(busy: true));
|
||||
try {
|
||||
final quote = await _trips.quote(
|
||||
city: '',
|
||||
serviceClass: type.code,
|
||||
distanceKm: route.distanceKm,
|
||||
durationMin: route.durationMin,
|
||||
);
|
||||
if (isClosed) return;
|
||||
// 200 بقيم `null` ليس نجاحاً — لا يُعرض رقم غير صالح للمستخدم.
|
||||
emit(state.copyWith(
|
||||
quote: quote,
|
||||
busy: false,
|
||||
error: quote.isUsable ? RideError.none : RideError.quoteFailed,
|
||||
));
|
||||
} on ApiException {
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(busy: false, error: RideError.quoteFailed));
|
||||
}
|
||||
}
|
||||
final results = await Future.wait(
|
||||
types.map((type) async {
|
||||
try {
|
||||
final quote = await _trips.quote(
|
||||
city: AppConfig.defaultCity,
|
||||
serviceClass: type.code,
|
||||
distanceKm: route.distanceKm,
|
||||
durationMin: route.durationMin,
|
||||
);
|
||||
// 200 بقيم `null` ليس نجاحاً — لا يُعرض رقم غير صالح للمستخدم.
|
||||
return quote.isUsable ? MapEntry(type.code, quote) : null;
|
||||
} on ApiException {
|
||||
// نوعٌ بلا تعرفة نشطة لا يُسقط الباقي — يُخفى سعره وحده.
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
if (isClosed) return;
|
||||
|
||||
final quotes = Map<String, FareQuote>.fromEntries(
|
||||
results.whereType<MapEntry<String, FareQuote>>(),
|
||||
);
|
||||
final selected = state.selectedRideType;
|
||||
|
||||
emit(state.copyWith(
|
||||
quotes: quotes,
|
||||
quote: selected == null ? null : quotes[selected.code],
|
||||
busy: false,
|
||||
error: quotes.isEmpty ? RideError.quoteFailed : RideError.none,
|
||||
));
|
||||
}
|
||||
|
||||
// ── الطلب والبحث ───────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user