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
@@ -41,6 +41,14 @@ class AppConfig {
|
||||
defaultValue: 'in_9478b32836d19cff73db3063',
|
||||
);
|
||||
|
||||
/// المدينة الافتراضية للتعرفة.
|
||||
///
|
||||
/// ⚠️ الخادم يبذر التعرفة بـ`city: 'default'` (`tenants.service.ts:156`)
|
||||
/// ويستعمل `dto.city ?? 'default'` عند إنشاء الرحلة. إرسال نصّ فارغ أو اسم
|
||||
/// مدينة حقيقي يرجّع **404 «No active tariff»** — وهو ما كان يظهر
|
||||
/// للمستخدم كـ«تعذّر حساب الأجرة».
|
||||
static const String defaultCity = 'default';
|
||||
|
||||
/// كل كم متر يُرفع موقع السائق. أقل من ذلك يستنزف البطارية بلا فائدة.
|
||||
static const int locationFilterMeters = 25;
|
||||
|
||||
|
||||
@@ -193,5 +193,12 @@
|
||||
"chatHint": "اكتب رسالة",
|
||||
"chatEmpty": "لا رسائل بعد",
|
||||
"rideCoupon": "كود خصم",
|
||||
"rideCouponApply": "تطبيق"
|
||||
"rideCouponApply": "تطبيق",
|
||||
"rideNoLocation": "فعّل الموقع لنعرف أين أنت",
|
||||
"rideBack": "رجوع",
|
||||
"rideRetryQuote": "أعد حساب الأجرة",
|
||||
"rideFrom": "من",
|
||||
"rideTo": "إلى",
|
||||
"walletChannel": "طريقة الدفع",
|
||||
"walletTopupNote": "الشحن التجريبي حالياً بلا تحصيل فعلي — ربط البوابة قادم"
|
||||
}
|
||||
@@ -193,5 +193,12 @@
|
||||
"chatHint": "Type a message",
|
||||
"chatEmpty": "No messages yet",
|
||||
"rideCoupon": "Promo code",
|
||||
"rideCouponApply": "Apply"
|
||||
"rideCouponApply": "Apply",
|
||||
"rideNoLocation": "Turn on location so we know where you are",
|
||||
"rideBack": "Back",
|
||||
"rideRetryQuote": "Recalculate fare",
|
||||
"rideFrom": "From",
|
||||
"rideTo": "To",
|
||||
"walletChannel": "Payment method",
|
||||
"walletTopupNote": "Top-up is currently a test cash-in with no real charge — gateway wiring is next"
|
||||
}
|
||||
@@ -8,7 +8,20 @@ import '../config.dart';
|
||||
class LocationService {
|
||||
StreamSubscription<Position>? _sub;
|
||||
|
||||
/// بلا هذا الفحص يرمي `getCurrentPosition` ونبتلع الخطأ فتبقى الكاميرا
|
||||
/// على الاحتياطي بلا سبب ظاهر — وهو ما كان يبدو «الخريطة لا تتبع موقعي».
|
||||
Future<bool> ensureReady() async {
|
||||
if (!await Geolocator.isLocationServiceEnabled()) return false;
|
||||
var permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
}
|
||||
return permission == LocationPermission.always ||
|
||||
permission == LocationPermission.whileInUse;
|
||||
}
|
||||
|
||||
Future<({double lat, double lng})?> current() async {
|
||||
if (!await ensureReady()) return null;
|
||||
try {
|
||||
final p = await Geolocator.getCurrentPosition(
|
||||
locationSettings: const LocationSettings(
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/// قنوات الدفع والسحب حسب **حزمة دولة المستأجر** (`countryPack` من
|
||||
/// `GET /tenant/config/:slug` — docs/38 §10)، لا حسب لغة الجهاز ولا موقعه.
|
||||
///
|
||||
/// المستأجر هو من يحدّد السوق: مشغّل أردني يخدم مستخدماً هاتفه إنجليزي
|
||||
/// وموقعه خارج الأردن يبقى أردنيّ القنوات.
|
||||
///
|
||||
/// ⚠️ **`POST /wallet/topup` شحنٌ تجريبي بلا مزوّد دفع خلفه** — الخادم نفسه
|
||||
/// يصفه بـ«كاش-إن، للإنتاج يُربط بمزوّد دفع أو يُقيَّد للأدمن». اختيار
|
||||
/// القناة هنا يجهّز الواجهة والعقد؛ التحصيل الفعلي عبر `/payments/charge`
|
||||
/// وبوابة كل قناة عملٌ منفصل لم يُنفَّذ بعد.
|
||||
enum PaymentChannel {
|
||||
cliq('cliq', 'CliQ', 'كليك'),
|
||||
syriatel('syriatel_cash', 'Syriatel Cash', 'سيرياتيل كاش'),
|
||||
mtn('mtn_cash', 'MTN Cash', 'إم تي إن كاش'),
|
||||
shamCash('sham_cash', 'Sham Cash', 'شام كاش'),
|
||||
paymob('paymob', 'PayMob', 'باي موب'),
|
||||
cash('cash', 'Cash', 'نقداً');
|
||||
|
||||
const PaymentChannel(this.wire, this.nameEn, this.nameAr);
|
||||
|
||||
/// القيمة المرسلة للخادم في حقل `channel`.
|
||||
final String wire;
|
||||
final String nameEn;
|
||||
final String nameAr;
|
||||
|
||||
String nameFor(String languageCode) =>
|
||||
languageCode == 'ar' ? nameAr : nameEn;
|
||||
|
||||
/// القنوات المتاحة لكل حزمة دولة — مطابقة لما هو مدعوم فعلياً في السوق
|
||||
/// (نفس قنوات سيرو الميدانية).
|
||||
static List<PaymentChannel> forCountry(String countryPack) =>
|
||||
switch (countryPack) {
|
||||
'jo' => const [PaymentChannel.cliq, PaymentChannel.cash],
|
||||
'sy' => const [
|
||||
PaymentChannel.syriatel,
|
||||
PaymentChannel.mtn,
|
||||
PaymentChannel.shamCash,
|
||||
PaymentChannel.cash,
|
||||
],
|
||||
'eg' => const [PaymentChannel.paymob, PaymentChannel.cash],
|
||||
// حزمة غير معروفة: النقد وحده — لا نعرض قناة قد لا تعمل في سوقها.
|
||||
_ => const [PaymentChannel.cash],
|
||||
};
|
||||
|
||||
/// قنوات السحب — النقد ليس قناة سحب.
|
||||
static List<PaymentChannel> payoutFor(String countryPack) =>
|
||||
forCountry(countryPack)
|
||||
.where((c) => c != PaymentChannel.cash)
|
||||
.toList(growable: false);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../design/tokens.dart';
|
||||
import '../design/tripz_colors.dart';
|
||||
import '../payments/payment_channels.dart';
|
||||
|
||||
/// اختيار قناة الدفع/السحب. القائمة تأتي من حزمة دولة المستأجر، فلا يرى
|
||||
/// الأردنيُّ «شام كاش» ولا السوريُّ «كليك».
|
||||
class ChannelPicker extends StatelessWidget {
|
||||
const ChannelPicker({
|
||||
super.key,
|
||||
required this.channels,
|
||||
required this.selected,
|
||||
required this.onSelect,
|
||||
});
|
||||
|
||||
final List<PaymentChannel> channels;
|
||||
final PaymentChannel? selected;
|
||||
final ValueChanged<PaymentChannel> onSelect;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (channels.isEmpty) return const SizedBox.shrink();
|
||||
final lang = Localizations.localeOf(context).languageCode;
|
||||
|
||||
return Wrap(
|
||||
spacing: Space.xs,
|
||||
runSpacing: Space.xs,
|
||||
children: [
|
||||
for (final channel in channels)
|
||||
ChoiceChip(
|
||||
selected: channel == selected,
|
||||
onSelected: (_) => onSelect(channel),
|
||||
avatar: Icon(_iconOf(channel), size: Sizes.iconSm),
|
||||
label: Text(channel.nameFor(lang)),
|
||||
labelStyle: context.texts.labelLarge,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
static IconData _iconOf(PaymentChannel channel) => switch (channel) {
|
||||
PaymentChannel.cash => Icons.payments_outlined,
|
||||
PaymentChannel.cliq => Icons.account_balance_outlined,
|
||||
PaymentChannel.paymob => Icons.credit_card_outlined,
|
||||
_ => Icons.smartphone_outlined,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user