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:
Hamza-Ayed
2026-08-05 00:36:02 +03:00
co-authored by Claude Opus 5
parent 29f375b122
commit 8976821a93
18 changed files with 731 additions and 87 deletions
@@ -4,6 +4,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/config.dart';
import '../../../../core/design/tokens.dart';
import '../../../../core/design/tripz_colors.dart';
import '../../../../core/payments/payment_channels.dart';
import '../../../../core/tenant/tenant_cubit.dart';
import '../../../../core/ui/channel_picker.dart';
import '../../../../core/l10n/l10n.dart';
import '../../../../core/ui/tripz_button.dart';
import '../../../../core/ui/tripz_otp_field.dart';
@@ -24,6 +27,7 @@ class PayoutSheet extends StatefulWidget {
class _PayoutSheetState extends State<PayoutSheet> {
final _amount = TextEditingController();
final _code = TextEditingController();
PaymentChannel? _channel;
@override
void dispose() {
@@ -75,7 +79,7 @@ class _PayoutSheetState extends State<PayoutSheet> {
hasError: state.failure == PayoutFailure.rejected,
onCompleted: cubit.confirm,
)
else
else ...[
TripzTextField(
controller: _amount,
label: l10n.payoutAmount,
@@ -85,6 +89,22 @@ class _PayoutSheetState extends State<PayoutSheet> {
decimal: true,
),
),
const SizedBox(height: Space.md),
Text(l10n.payoutChannel, style: context.texts.titleSmall),
const SizedBox(height: Space.xs),
Builder(builder: (context) {
// قنوات السحب من حزمة دولة المستأجر — والنقد ليس قناة سحب.
final channels = PaymentChannel.payoutFor(
context.watch<TenantCubit>().state.countryPack,
);
_channel ??= channels.isEmpty ? null : channels.first;
return ChannelPicker(
channels: channels,
selected: _channel,
onSelect: (c) => setState(() => _channel = c),
);
}),
],
if (state.failure != PayoutFailure.none) ...[
const SizedBox(height: Space.sm),
Text(
@@ -107,8 +127,9 @@ class _PayoutSheetState extends State<PayoutSheet> {
cubit.confirm(_code.text.trim());
} else {
final amount = num.tryParse(_amount.text.trim());
if (amount != null) {
cubit.request(amount: amount, channel: 'cliq');
final channel = _channel;
if (amount != null && channel != null) {
cubit.request(amount: amount, channel: channel.wire);
}
}
},