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
@@ -12,6 +12,7 @@ import '../../../core/l10n/l10n.dart';
|
||||
|
||||
import '../../../core/ui/app_drawer.dart';
|
||||
|
||||
import '../../../core/ui/status_banner.dart';
|
||||
import '../../../core/ui/tripz_button.dart';
|
||||
|
||||
import '../cubit/ride_cubit.dart';
|
||||
@@ -44,16 +45,32 @@ class RidePage extends StatefulWidget {
|
||||
State<RidePage> createState() => _RidePageState();
|
||||
}
|
||||
|
||||
class _RidePageState extends State<RidePage> {
|
||||
class _RidePageState extends State<RidePage> with WidgetsBindingObserver {
|
||||
late final RideCubit _cubit = sl<RideCubit>()..init();
|
||||
GeoPoint? _mapCenter;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_cubit.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState lifecycle) {
|
||||
// الإذن قد يُمنح من إعدادات النظام والتطبيق في الخلفية — نلتقطه عند
|
||||
// العودة بدل أن تبقى الخريطة على الموقع الاحتياطي.
|
||||
if (lifecycle == AppLifecycleState.resumed && _cubit.state.origin == null) {
|
||||
_cubit.locateMe();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
@@ -78,6 +95,9 @@ class _RidePageState extends State<RidePage> {
|
||||
route: data.route,
|
||||
cameraTarget: data.camera,
|
||||
onCameraIdle: (p) => _mapCenter = p,
|
||||
// الورقة تغطّي نحو ثلث الشاشة — يُحسب منه إطار المسار كي لا
|
||||
// يختفي نصفه تحتها.
|
||||
bottomInset: MediaQuery.sizeOf(context).height * 0.34,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -159,7 +179,8 @@ class _Sheet extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final child = switch (state.phase) {
|
||||
RidePhase.idle => const _Collapsed(),
|
||||
RidePhase.idle =>
|
||||
_Collapsed(noLocation: state.error == RideError.noLocation),
|
||||
RidePhase.planning => PlannerSheet(state: state),
|
||||
RidePhase.pickingOnMap => _PickConfirm(onConfirm: onConfirmPick),
|
||||
RidePhase.confirming => ConfirmSheet(state: state),
|
||||
@@ -217,15 +238,37 @@ class _MapFab extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _Collapsed extends StatelessWidget {
|
||||
const _Collapsed();
|
||||
const _Collapsed({required this.noLocation});
|
||||
|
||||
final bool noLocation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TripzButton.secondary(
|
||||
label: context.l10n.rideWhereTo,
|
||||
icon: Icons.search_rounded,
|
||||
expanded: true,
|
||||
onPressed: context.read<RideCubit>().openPlanner,
|
||||
final cubit = context.read<RideCubit>();
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// بلا موقع لا خريطة مفيدة — السبب يُعرض مع مخرجه بدل خريطة صامتة
|
||||
// على موقع خاطئ.
|
||||
if (noLocation) ...[
|
||||
StatusBanner(
|
||||
message: context.l10n.rideNoLocation,
|
||||
tone: BannerTone.warning,
|
||||
icon: Icons.location_off_outlined,
|
||||
trailing: TripzButton.text(
|
||||
label: context.l10n.actionRetry,
|
||||
onPressed: cubit.locateMe,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: Space.sm),
|
||||
],
|
||||
TripzButton.secondary(
|
||||
label: context.l10n.rideWhereTo,
|
||||
icon: Icons.search_rounded,
|
||||
expanded: true,
|
||||
onPressed: cubit.openPlanner,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user