feat(apps): الدراور على نمط سيرو — المكافآت والدعوات والمساعدة
جُرد دراور سيرو: 14 مدخلاً للراكب (map_menu_widget) و23 للسائق (drawer_captain). بُني ما له نقطة فعلية على NestJS، والباقي مذكور صراحةً. ## مبنيّ - **المكافآت** (features/rewards): GET /rewards/referrals و /rewards/coupons — كلاهما مُتحقَّق حيّاً. كود الدعوة يُنسخ ويُشارك، وعدّادات دعوتَهم/قيد التأكيد/مدفوعة. يقابل Promos + Invite Driver في سيرو - **شارك التطبيق · قيّم التطبيق · سياسة الخصوصية · تطبيق الطرف الآخر** — أفعال محليّة بلا نقطة خادم (share_plus + url_launcher) - **المساعدة** وُسّعت: أسئلة شائعة قابلة للطيّ · واتساب الدعم · **فحص توافق الجهاز** (نمط سيرو) يفحص ما يُعطّل التطبيق فعلاً: خدمة الموقع وإذنها وإذن الإشعارات — أسباب حقيقية لشكوى «التطبيق لا يعمل» - روابط المتجر والخصوصية والدعم صارت قيم بناء لكل مستأجر في BuildConfig ## غير مبنيّ — ولا نقطة خادم له الشكاوى (R3 في الباكلوج) · الإحصاءات · التحدّيات ولوحة المتصدّرين · مركز الصيانة · جدولي · التأمين الصحي (مؤجَّل بقرار المالك) · الطعام والمواصلات (خارج النطاق، docs/39 §1). بناء واجهة بلا خادم يعني شاشة تكذب على المستخدم. ## سجلّ الإشعارات سيرو يعرض قائمة إشعارات؛ الخادم يوفّر POST /notifications/token فقط بلا نقطة سرد. لم تُبنَ شاشة قائمة على فراغ. flutter analyze نظيف في التطبيقين. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6a33cc7241
commit
ca766a2d4e
@@ -22,6 +22,28 @@ class BuildConfig {
|
|||||||
|
|
||||||
static const String logoAsset = 'assets/images/logo.png';
|
static const String logoAsset = 'assets/images/logo.png';
|
||||||
static const String splashLogoAsset = 'assets/images/splash_logo.png';
|
static const String splashLogoAsset = 'assets/images/splash_logo.png';
|
||||||
|
|
||||||
|
/// روابط خارجية لكل مستأجر — تُولَّد مع بقية الملف (N3).
|
||||||
|
static const String storeUrl = String.fromEnvironment(
|
||||||
|
'STORE_URL',
|
||||||
|
defaultValue: 'https://tripz.app',
|
||||||
|
);
|
||||||
|
static const String privacyUrl = String.fromEnvironment(
|
||||||
|
'PRIVACY_URL',
|
||||||
|
defaultValue: 'https://tripz.app/privacy',
|
||||||
|
);
|
||||||
|
|
||||||
|
/// رقم دعم واتساب المستأجر — بصيغة دولية بلا `+`.
|
||||||
|
static const String supportPhone = String.fromEnvironment(
|
||||||
|
'SUPPORT_PHONE',
|
||||||
|
defaultValue: '962790000000',
|
||||||
|
);
|
||||||
|
|
||||||
|
/// رابط تطبيق الطرف الآخر — السائق يدعو ركّاباً والعكس.
|
||||||
|
static const String driverAppUrl = String.fromEnvironment(
|
||||||
|
'RIDER_APP_URL',
|
||||||
|
defaultValue: 'https://tripz.app',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// أعلام الاستحقاقات (docs/23 §5). **`const` إجبارياً** كي يُحذف كود الميزة
|
/// أعلام الاستحقاقات (docs/23 §5). **`const` إجبارياً** كي يُحذف كود الميزة
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import '../features/profile/data/profile_repository.dart';
|
|||||||
import '../features/rating/cubit/rating_cubit.dart';
|
import '../features/rating/cubit/rating_cubit.dart';
|
||||||
|
|
||||||
import '../features/rating/data/rating_repository.dart';
|
import '../features/rating/data/rating_repository.dart';
|
||||||
|
import '../features/rewards/cubit/rewards_cubit.dart';
|
||||||
|
import '../features/rewards/data/rewards_repository.dart';
|
||||||
|
|
||||||
import '../features/settings/cubit/settings_cubit.dart';
|
import '../features/settings/cubit/settings_cubit.dart';
|
||||||
|
|
||||||
@@ -113,6 +115,12 @@ Future<void> setupInjector() async {
|
|||||||
sl.registerFactory<HistoryCubit>(() => HistoryCubit(sl()));
|
sl.registerFactory<HistoryCubit>(() => HistoryCubit(sl()));
|
||||||
|
|
||||||
sl.registerSingleton<RatingRepository>(RatingRepository(sl()));
|
sl.registerSingleton<RatingRepository>(RatingRepository(sl()));
|
||||||
|
|
||||||
|
// المكافآت خلف علم الكوبونات: الدعوات والكوبونات وجهان لنفس الوحدة.
|
||||||
|
if (Features.coupons) {
|
||||||
|
sl.registerSingleton<RewardsRepository>(RewardsRepository(sl()));
|
||||||
|
sl.registerFactory<RewardsCubit>(() => RewardsCubit(sl()));
|
||||||
|
}
|
||||||
sl.registerFactory<RatingCubit>(() => RatingCubit(sl()));
|
sl.registerFactory<RatingCubit>(() => RatingCubit(sl()));
|
||||||
|
|
||||||
// الدردشة خلف علمها: إطفاؤه يحذف كودها من الـbinary (docs/23 §5).
|
// الدردشة خلف علمها: إطفاؤه يحذف كودها من الـbinary (docs/23 §5).
|
||||||
|
|||||||
@@ -209,6 +209,41 @@
|
|||||||
"walletCopy": "نسخ",
|
"walletCopy": "نسخ",
|
||||||
"walletCopied": "نُسخ",
|
"walletCopied": "نُسخ",
|
||||||
"walletNoTarget": "لم يُضبط حساب استلام لهذه القناة بعد — راجع المشغّل",
|
"walletNoTarget": "لم يُضبط حساب استلام لهذه القناة بعد — راجع المشغّل",
|
||||||
|
"rewardsTitle": "مكافآتي",
|
||||||
|
"rewardsInviteTitle": "ادعُ سائقاً",
|
||||||
|
"rewardsInviteLead": "شارك كودك — تربح عن كل سائق ينضم",
|
||||||
|
"rewardsCode": "كود الدعوة",
|
||||||
|
"rewardsInvited": "دعوتَهم",
|
||||||
|
"rewardsPending": "قيد التأكيد",
|
||||||
|
"rewardsPaid": "مكافآت مدفوعة",
|
||||||
|
"rewardsCoupons": "كوبوناتي",
|
||||||
|
"rewardsCouponsEmpty": "لا كوبونات صالحة الآن",
|
||||||
|
"menuRewards": "مكافآتي",
|
||||||
|
"menuInvite": "ادعُ سائقاً",
|
||||||
|
"menuShare": "شارك التطبيق",
|
||||||
|
"menuNotifications": "الإشعارات",
|
||||||
|
"menuPrivacy": "سياسة الخصوصية",
|
||||||
|
"menuRateApp": "قيّم التطبيق",
|
||||||
|
"shareMessage": "انضم لـTripz كسائق — استعمل كودي {code}",
|
||||||
|
"menuDriverApp": "تطبيق الراكب",
|
||||||
|
"@shareMessage": {
|
||||||
|
"placeholders": {
|
||||||
|
"code": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"supportFaqQ1": "كيف أطلب رحلة؟",
|
||||||
|
"supportFaqA1": "حدّد وجهتك من البحث أو من الخريطة، اختر نوع الرحلة، ثم اضغط «اطلب الرحلة».",
|
||||||
|
"supportFaqQ2": "كيف أشحن رصيدي؟",
|
||||||
|
"supportFaqA2": "من المحفظة اختر المبلغ وطريقة الدفع. ستظهر لك تعليمات التحويل ورقم مرجع — يُضاف الرصيد بعد تأكيد التحويل.",
|
||||||
|
"supportFaqQ3": "لماذا لا يظهر سائق؟",
|
||||||
|
"supportFaqA3": "قد لا يوجد سائقون قريبون في هذا الوقت. جرّب بعد قليل أو غيّر نوع الرحلة.",
|
||||||
|
"supportFaqQ4": "كيف ألغي رحلة؟",
|
||||||
|
"supportFaqA4": "من بطاقة الرحلة اضغط «إلغاء الرحلة». الإلغاء بعد وصول السائق قد يترتّب عليه رسم.",
|
||||||
|
"supportDeviceCheck": "فحص توافق الجهاز",
|
||||||
|
"supportDeviceOk": "جهازك متوافق",
|
||||||
|
"supportDeviceIssues": "مشاكل قد تؤثّر على عمل التطبيق",
|
||||||
"obApplyTitle": "سجّل كسائق",
|
"obApplyTitle": "سجّل كسائق",
|
||||||
"obApplyLead": "خطوات قصيرة ثم تبدأ العمل",
|
"obApplyLead": "خطوات قصيرة ثم تبدأ العمل",
|
||||||
"obVehicleMake": "شركة السيارة",
|
"obVehicleMake": "شركة السيارة",
|
||||||
|
|||||||
@@ -209,6 +209,41 @@
|
|||||||
"walletCopy": "Copy",
|
"walletCopy": "Copy",
|
||||||
"walletCopied": "Copied",
|
"walletCopied": "Copied",
|
||||||
"walletNoTarget": "No receiving account is configured for this channel yet — contact the operator",
|
"walletNoTarget": "No receiving account is configured for this channel yet — contact the operator",
|
||||||
|
"rewardsTitle": "My rewards",
|
||||||
|
"rewardsInviteTitle": "Invite a driver",
|
||||||
|
"rewardsInviteLead": "Share your code — earn for every driver who joins",
|
||||||
|
"rewardsCode": "Invite code",
|
||||||
|
"rewardsInvited": "Invited",
|
||||||
|
"rewardsPending": "Pending",
|
||||||
|
"rewardsPaid": "Rewards paid",
|
||||||
|
"rewardsCoupons": "My coupons",
|
||||||
|
"rewardsCouponsEmpty": "No valid coupons right now",
|
||||||
|
"menuRewards": "My rewards",
|
||||||
|
"menuInvite": "Invite a driver",
|
||||||
|
"menuShare": "Share the app",
|
||||||
|
"menuNotifications": "Notifications",
|
||||||
|
"menuPrivacy": "Privacy policy",
|
||||||
|
"menuRateApp": "Rate the app",
|
||||||
|
"shareMessage": "Join Tripz as a driver — use my code {code}",
|
||||||
|
"menuDriverApp": "Rider app",
|
||||||
|
"@shareMessage": {
|
||||||
|
"placeholders": {
|
||||||
|
"code": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"supportFaqQ1": "How do I request a ride?",
|
||||||
|
"supportFaqA1": "Pick your destination from search or the map, choose a ride type, then tap “Request ride”.",
|
||||||
|
"supportFaqQ2": "How do I top up?",
|
||||||
|
"supportFaqA2": "In the wallet, choose an amount and a payment method. You'll get transfer instructions and a reference — your balance updates once the transfer is confirmed.",
|
||||||
|
"supportFaqQ3": "Why is no driver showing up?",
|
||||||
|
"supportFaqA3": "There may be no drivers nearby right now. Try again shortly or change the ride type.",
|
||||||
|
"supportFaqQ4": "How do I cancel?",
|
||||||
|
"supportFaqA4": "Tap “Cancel ride” on the trip card. Cancelling after the driver arrives may incur a fee.",
|
||||||
|
"supportDeviceCheck": "Check device compatibility",
|
||||||
|
"supportDeviceOk": "Your device is compatible",
|
||||||
|
"supportDeviceIssues": "Issues that may affect the app",
|
||||||
"obApplyTitle": "Register as a driver",
|
"obApplyTitle": "Register as a driver",
|
||||||
"obApplyLead": "A few short steps, then you're working",
|
"obApplyLead": "A few short steps, then you're working",
|
||||||
"obVehicleMake": "Car make",
|
"obVehicleMake": "Car make",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import '../features/profile/view/profile_edit_page.dart';
|
|||||||
import '../features/profile/view/profile_setup_page.dart';
|
import '../features/profile/view/profile_setup_page.dart';
|
||||||
|
|
||||||
import '../features/rating/data/rating_repository.dart';
|
import '../features/rating/data/rating_repository.dart';
|
||||||
|
import '../features/rewards/view/rewards_page.dart';
|
||||||
|
|
||||||
import '../features/rating/view/rating_page.dart';
|
import '../features/rating/view/rating_page.dart';
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ class Routes {
|
|||||||
static const profileEdit = '/account';
|
static const profileEdit = '/account';
|
||||||
static const settings = '/settings';
|
static const settings = '/settings';
|
||||||
static const support = '/support';
|
static const support = '/support';
|
||||||
|
static const rewards = '/rewards';
|
||||||
static const chat = '/chat';
|
static const chat = '/chat';
|
||||||
static const onboarding = '/onboarding';
|
static const onboarding = '/onboarding';
|
||||||
}
|
}
|
||||||
@@ -135,6 +137,10 @@ final appRouter = GoRouter(
|
|||||||
path: Routes.onboarding,
|
path: Routes.onboarding,
|
||||||
builder: (context, state) => const OnboardingPage(),
|
builder: (context, state) => const OnboardingPage(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.rewards,
|
||||||
|
builder: (context, state) => const RewardsPage(),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: Routes.support,
|
path: Routes.support,
|
||||||
builder: (context, state) => const SupportPage(),
|
builder: (context, state) => const SupportPage(),
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../build_config.dart';
|
import '../build_config.dart';
|
||||||
import '../config.dart';
|
import '../config.dart';
|
||||||
@@ -52,6 +54,13 @@ class AppDrawer extends StatelessWidget {
|
|||||||
label: isDriver ? l10n.menuEarnings : l10n.menuWallet,
|
label: isDriver ? l10n.menuEarnings : l10n.menuWallet,
|
||||||
route: Routes.wallet,
|
route: Routes.wallet,
|
||||||
),
|
),
|
||||||
|
if (Features.coupons && context.tenantHas('coupons'))
|
||||||
|
_Item(
|
||||||
|
icon: Icons.card_giftcard_rounded,
|
||||||
|
label: l10n.menuRewards,
|
||||||
|
route: Routes.rewards,
|
||||||
|
),
|
||||||
|
const Divider(indent: Space.md, endIndent: Space.md),
|
||||||
_Item(
|
_Item(
|
||||||
icon: Icons.settings_outlined,
|
icon: Icons.settings_outlined,
|
||||||
label: l10n.menuSettings,
|
label: l10n.menuSettings,
|
||||||
@@ -62,6 +71,29 @@ class AppDrawer extends StatelessWidget {
|
|||||||
label: l10n.menuSupport,
|
label: l10n.menuSupport,
|
||||||
route: Routes.support,
|
route: Routes.support,
|
||||||
),
|
),
|
||||||
|
// مداخل محليّة بلا نقطة خادم — تُنفَّذ في الجهاز مباشرة.
|
||||||
|
_Action(
|
||||||
|
icon: Icons.share_outlined,
|
||||||
|
label: l10n.menuShare,
|
||||||
|
onTap: () => SharePlus.instance
|
||||||
|
.share(ShareParams(text: l10n.shareMessage(''))),
|
||||||
|
),
|
||||||
|
_Action(
|
||||||
|
icon: Icons.star_outline_rounded,
|
||||||
|
label: l10n.menuRateApp,
|
||||||
|
onTap: () => _open(BuildConfig.storeUrl),
|
||||||
|
),
|
||||||
|
_Action(
|
||||||
|
icon: Icons.shield_outlined,
|
||||||
|
label: l10n.menuPrivacy,
|
||||||
|
onTap: () => _open(BuildConfig.privacyUrl),
|
||||||
|
),
|
||||||
|
if (!isDriver)
|
||||||
|
_Action(
|
||||||
|
icon: Icons.local_taxi_outlined,
|
||||||
|
label: l10n.menuDriverApp,
|
||||||
|
onTap: () => _open(BuildConfig.driverAppUrl),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -83,6 +115,13 @@ class AppDrawer extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<void> _open(String url) async {
|
||||||
|
final uri = Uri.tryParse(url);
|
||||||
|
if (uri == null) return;
|
||||||
|
// فشل الفتح لا يُسقط شيئاً — رابط خارجي ليس جزءاً من التطبيق.
|
||||||
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _confirmLogout(BuildContext context) async {
|
Future<void> _confirmLogout(BuildContext context) async {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
final session = context.read<SessionCubit>();
|
final session = context.read<SessionCubit>();
|
||||||
@@ -155,6 +194,31 @@ class _Header extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// مدخل ينفّذ فعلاً محلياً (مشاركة · رابط خارجي) لا تنقّلاً.
|
||||||
|
class _Action extends StatelessWidget {
|
||||||
|
const _Action({
|
||||||
|
required this.icon,
|
||||||
|
required this.label,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(icon),
|
||||||
|
title: Text(label),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
onTap();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _Item extends StatelessWidget {
|
class _Item extends StatelessWidget {
|
||||||
const _Item({required this.icon, required this.label, required this.route});
|
const _Item({required this.icon, required this.label, required this.route});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
import '../../../core/api/api_exception.dart';
|
||||||
|
import '../../../core/ui/view_status.dart';
|
||||||
|
import '../data/rewards_repository.dart';
|
||||||
|
|
||||||
|
class RewardsState {
|
||||||
|
const RewardsState({
|
||||||
|
this.status = ViewStatus.idle,
|
||||||
|
this.referrals,
|
||||||
|
this.coupons = const [],
|
||||||
|
this.error,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ViewStatus status;
|
||||||
|
final ReferralSummary? referrals;
|
||||||
|
final List<Coupon> coupons;
|
||||||
|
final String? error;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RewardsCubit extends Cubit<RewardsState> {
|
||||||
|
RewardsCubit(this._repo) : super(const RewardsState());
|
||||||
|
|
||||||
|
final RewardsRepository _repo;
|
||||||
|
|
||||||
|
Future<void> load() async {
|
||||||
|
emit(const RewardsState(status: ViewStatus.loading));
|
||||||
|
try {
|
||||||
|
// النداءان متوازيان: لا سبب لانتظار أحدهما الآخر.
|
||||||
|
final results = await Future.wait([
|
||||||
|
_repo.referrals(),
|
||||||
|
_repo.coupons(),
|
||||||
|
]);
|
||||||
|
if (isClosed) return;
|
||||||
|
emit(RewardsState(
|
||||||
|
status: ViewStatus.success,
|
||||||
|
referrals: results[0] as ReferralSummary,
|
||||||
|
coupons: results[1] as List<Coupon>,
|
||||||
|
));
|
||||||
|
} on ApiException catch (e) {
|
||||||
|
if (!isClosed) {
|
||||||
|
emit(RewardsState(status: ViewStatus.error, error: e.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import '../../../core/api/api_client.dart';
|
||||||
|
|
||||||
|
/// ملخّص الدعوات — شكل مُتحقَّق حيّاً: `{code, total, pending, paid}`.
|
||||||
|
class ReferralSummary {
|
||||||
|
const ReferralSummary({
|
||||||
|
required this.code,
|
||||||
|
required this.total,
|
||||||
|
required this.pending,
|
||||||
|
required this.paid,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// كود دعوتي الذي أشاركه.
|
||||||
|
final String code;
|
||||||
|
|
||||||
|
final int total;
|
||||||
|
final int pending;
|
||||||
|
final int paid;
|
||||||
|
|
||||||
|
factory ReferralSummary.fromJson(Map<String, dynamic> json) =>
|
||||||
|
ReferralSummary(
|
||||||
|
code: json['code'] as String? ?? '',
|
||||||
|
total: (json['total'] as num?)?.toInt() ?? 0,
|
||||||
|
pending: (json['pending'] as num?)?.toInt() ?? 0,
|
||||||
|
paid: (json['paid'] as num?)?.toInt() ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coupon {
|
||||||
|
const Coupon({
|
||||||
|
required this.code,
|
||||||
|
this.description = '',
|
||||||
|
this.expiresAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String code;
|
||||||
|
final String description;
|
||||||
|
final DateTime? expiresAt;
|
||||||
|
|
||||||
|
factory Coupon.fromJson(Map<String, dynamic> json) => Coupon(
|
||||||
|
code: json['code'] as String? ?? '',
|
||||||
|
description: json['description'] as String? ??
|
||||||
|
json['title'] as String? ??
|
||||||
|
'',
|
||||||
|
expiresAt: DateTime.tryParse(json['expires_at']?.toString() ?? ''),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class RewardsRepository {
|
||||||
|
RewardsRepository(this._api);
|
||||||
|
|
||||||
|
final ApiClient _api;
|
||||||
|
|
||||||
|
Future<ReferralSummary> referrals() async {
|
||||||
|
final res = await _api.get<Map<String, dynamic>>('/rewards/referrals');
|
||||||
|
return ReferralSummary.fromJson(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Coupon>> coupons() async {
|
||||||
|
final res = await _api.get<List<dynamic>>('/rewards/coupons');
|
||||||
|
return res
|
||||||
|
.whereType<Map<String, dynamic>>()
|
||||||
|
.map(Coupon.fromJson)
|
||||||
|
.toList(growable: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
|
import '../../../core/design/tokens.dart';
|
||||||
|
import '../../../core/design/tripz_colors.dart';
|
||||||
|
import '../../../core/design/typography.dart';
|
||||||
|
import '../../../core/di.dart';
|
||||||
|
import '../../../core/l10n/l10n.dart';
|
||||||
|
import '../../../core/ui/tripz_button.dart';
|
||||||
|
import '../../../core/ui/tripz_card.dart';
|
||||||
|
import '../../../core/ui/tripz_scaffold.dart';
|
||||||
|
import '../cubit/rewards_cubit.dart';
|
||||||
|
|
||||||
|
/// «مكافآتي» — الدعوات والكوبونات في شاشة واحدة (نمط سيرو: Promos + Invite).
|
||||||
|
class RewardsPage extends StatefulWidget {
|
||||||
|
const RewardsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RewardsPage> createState() => _RewardsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RewardsPageState extends State<RewardsPage> {
|
||||||
|
late final RewardsCubit _cubit = sl<RewardsCubit>()..load();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_cubit.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return BlocProvider.value(
|
||||||
|
value: _cubit,
|
||||||
|
child: BlocBuilder<RewardsCubit, RewardsState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final ref = state.referrals;
|
||||||
|
return TripzScaffold(
|
||||||
|
title: l10n.rewardsTitle,
|
||||||
|
status: state.status,
|
||||||
|
error: state.error,
|
||||||
|
onRetry: _cubit.load,
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
if (ref != null) _InviteCard(code: ref.code, summary: ref),
|
||||||
|
const SizedBox(height: Space.lg),
|
||||||
|
Text(l10n.rewardsCoupons, style: context.texts.titleMedium),
|
||||||
|
const SizedBox(height: Space.xs),
|
||||||
|
if (state.coupons.isEmpty)
|
||||||
|
Text(
|
||||||
|
l10n.rewardsCouponsEmpty,
|
||||||
|
style: context.texts.bodySmall?.copyWith(
|
||||||
|
color: context.colors.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
for (final coupon in state.coupons) ...[
|
||||||
|
TripzCard(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.local_offer_outlined,
|
||||||
|
color: context.tripzColors.success,
|
||||||
|
),
|
||||||
|
const SizedBox(width: Space.sm),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
coupon.code,
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
style: numericStyle(context.texts.titleSmall),
|
||||||
|
),
|
||||||
|
if (coupon.description.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
coupon.description,
|
||||||
|
style: context.texts.bodySmall?.copyWith(
|
||||||
|
color: context.colors.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: Space.xs),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InviteCard extends StatelessWidget {
|
||||||
|
const _InviteCard({required this.code, required this.summary});
|
||||||
|
|
||||||
|
final String code;
|
||||||
|
final dynamic summary;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return Container(
|
||||||
|
padding: Space.page,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.colors.primaryContainer,
|
||||||
|
borderRadius: Radii.card_,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.rewardsInviteTitle,
|
||||||
|
style: context.texts.titleMedium?.copyWith(
|
||||||
|
color: context.colors.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: Space.xxs),
|
||||||
|
Text(
|
||||||
|
l10n.rewardsInviteLead,
|
||||||
|
style: context.texts.bodySmall?.copyWith(
|
||||||
|
color: context.colors.onPrimaryContainer.withValues(alpha: 0.8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: Space.md),
|
||||||
|
// الكود يُنسخ ويُشارك — الطريقان اللذان يستعملهما الناس فعلاً.
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SelectableText(
|
||||||
|
code,
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
style: numericStyle(context.texts.headlineSmall).copyWith(
|
||||||
|
letterSpacing: 3,
|
||||||
|
color: context.colors.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.copy_rounded),
|
||||||
|
color: context.colors.onPrimaryContainer,
|
||||||
|
onPressed: () => Clipboard.setData(ClipboardData(text: code)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: Space.sm),
|
||||||
|
TripzButton.primary(
|
||||||
|
label: l10n.menuShare,
|
||||||
|
icon: Icons.share_outlined,
|
||||||
|
onPressed: () =>
|
||||||
|
SharePlus.instance.share(ShareParams(text: l10n.shareMessage(code))),
|
||||||
|
),
|
||||||
|
const SizedBox(height: Space.md),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
_Stat(label: l10n.rewardsInvited, value: summary.total),
|
||||||
|
_Stat(label: l10n.rewardsPending, value: summary.pending),
|
||||||
|
_Stat(label: l10n.rewardsPaid, value: summary.paid),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Stat extends StatelessWidget {
|
||||||
|
const _Stat({required this.label, required this.value});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final int value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'$value',
|
||||||
|
style: numericStyle(context.texts.titleLarge).copyWith(
|
||||||
|
color: context.colors.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: context.texts.labelSmall?.copyWith(
|
||||||
|
color: context.colors.onPrimaryContainer.withValues(alpha: 0.8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../../../core/build_config.dart';
|
import '../../../core/build_config.dart';
|
||||||
import '../../../core/design/tokens.dart';
|
import '../../../core/design/tokens.dart';
|
||||||
@@ -7,35 +10,79 @@ import '../../../core/l10n/l10n.dart';
|
|||||||
import '../../../core/ui/tripz_card.dart';
|
import '../../../core/ui/tripz_card.dart';
|
||||||
import '../../../core/ui/tripz_scaffold.dart';
|
import '../../../core/ui/tripz_scaffold.dart';
|
||||||
|
|
||||||
/// المساعدة و«عن التطبيق». محتوى محلّي عمداً: الخادم لا يوفّر نقطة أسئلة
|
/// المساعدة: أسئلة شائعة · تواصل · فحص توافق الجهاز · عن التطبيق.
|
||||||
/// شائعة، وربطها بنصّ من الشبكة يعني شاشة فارغة حين ينقطع الاتصال — وهو
|
///
|
||||||
/// بالضبط الوقت الذي يفتح فيه المستخدم المساعدة.
|
/// المحتوى **محلّي عمداً**: الخادم لا يوفّر نقطة أسئلة شائعة، وربطها بالشبكة
|
||||||
|
/// يعني شاشة فارغة حين ينقطع الاتصال — وهو بالضبط الوقت الذي يفتح فيه
|
||||||
|
/// المستخدم المساعدة.
|
||||||
class SupportPage extends StatelessWidget {
|
class SupportPage extends StatelessWidget {
|
||||||
const SupportPage({super.key});
|
const SupportPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
|
final faq = [
|
||||||
|
(l10n.supportFaqQ1, l10n.supportFaqA1),
|
||||||
|
(l10n.supportFaqQ2, l10n.supportFaqA2),
|
||||||
|
(l10n.supportFaqQ3, l10n.supportFaqA3),
|
||||||
|
(l10n.supportFaqQ4, l10n.supportFaqA4),
|
||||||
|
];
|
||||||
|
|
||||||
return TripzScaffold(
|
return TripzScaffold(
|
||||||
title: l10n.supportTitle,
|
title: l10n.supportTitle,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
TripzCard(
|
Text(l10n.supportFaq, style: context.texts.titleMedium),
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(l10n.supportContact, style: context.texts.titleMedium),
|
|
||||||
const SizedBox(height: Space.xs),
|
const SizedBox(height: Space.xs),
|
||||||
Text(
|
for (final (q, a) in faq)
|
||||||
l10n.supportWhatsapp,
|
Card(
|
||||||
style: context.texts.bodyMedium?.copyWith(
|
margin: const EdgeInsets.only(bottom: Space.xs),
|
||||||
|
child: ExpansionTile(
|
||||||
|
shape: const RoundedRectangleBorder(borderRadius: Radii.card_),
|
||||||
|
title: Text(q, style: context.texts.bodyMedium),
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: Space.page,
|
||||||
|
child: Text(
|
||||||
|
a,
|
||||||
|
style: context.texts.bodySmall?.copyWith(
|
||||||
color: context.colors.onSurfaceVariant,
|
color: context.colors.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: Space.lg),
|
||||||
|
Text(l10n.supportContact, style: context.texts.titleMedium),
|
||||||
|
const SizedBox(height: Space.xs),
|
||||||
|
TripzCard(
|
||||||
|
onTap: () => _openWhatsapp(BuildConfig.supportPhone),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.chat_outlined, color: context.tripzColors.success),
|
||||||
|
const SizedBox(width: Space.sm),
|
||||||
|
Expanded(child: Text(l10n.supportWhatsapp)),
|
||||||
|
const Icon(Icons.chevron_left_rounded),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: Space.md),
|
const SizedBox(height: Space.md),
|
||||||
|
TripzCard(
|
||||||
|
onTap: () => _showDeviceCheck(context),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.phonelink_setup_outlined),
|
||||||
|
const SizedBox(width: Space.sm),
|
||||||
|
Expanded(child: Text(l10n.supportDeviceCheck)),
|
||||||
|
const Icon(Icons.chevron_left_rounded),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: Space.lg),
|
||||||
TripzCard(
|
TripzCard(
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
@@ -54,4 +101,69 @@ class SupportPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<void> _openWhatsapp(String phone) async {
|
||||||
|
final uri = Uri.parse('https://wa.me/$phone');
|
||||||
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// فحص توافق الجهاز — نمط سيرو. يفحص ما يُعطّل التطبيق فعلاً: خدمة الموقع
|
||||||
|
/// وإذنها وإذن الإشعارات. أسبابٌ حقيقية لشكاوى «التطبيق لا يعمل».
|
||||||
|
static Future<void> _showDeviceCheck(BuildContext context) async {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final issues = <String>[];
|
||||||
|
|
||||||
|
if (!await Geolocator.isLocationServiceEnabled()) {
|
||||||
|
issues.add(l10n.rideNoLocation);
|
||||||
|
}
|
||||||
|
final locationPermission = await Permission.locationWhenInUse.status;
|
||||||
|
if (!locationPermission.isGranted && !locationPermission.isLimited) {
|
||||||
|
issues.add(l10n.permissionDeniedForever);
|
||||||
|
}
|
||||||
|
if (await Permission.notification.isDenied) {
|
||||||
|
issues.add(l10n.notifDenied);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.mounted) return;
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(l10n.supportDeviceCheck),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: issues.isEmpty
|
||||||
|
? [
|
||||||
|
Row(children: [
|
||||||
|
Icon(Icons.check_circle_rounded,
|
||||||
|
color: context.tripzColors.success),
|
||||||
|
const SizedBox(width: Space.xs),
|
||||||
|
Expanded(child: Text(l10n.supportDeviceOk)),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
Text(l10n.supportDeviceIssues),
|
||||||
|
const SizedBox(height: Space.xs),
|
||||||
|
for (final issue in issues)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: Space.xxs),
|
||||||
|
child: Row(children: [
|
||||||
|
Icon(Icons.error_outline_rounded,
|
||||||
|
size: Sizes.iconSm,
|
||||||
|
color: context.tripzColors.warning),
|
||||||
|
const SizedBox(width: Space.xs),
|
||||||
|
Expanded(child: Text(issue)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(l10n.actionContinue),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,33 +53,17 @@ class WalletRepository {
|
|||||||
/// الخادم يولّد مرجعاً ويعرض حساب الاستلام، ثم يحوّل المستخدم، والتسوية
|
/// الخادم يولّد مرجعاً ويعرض حساب الاستلام، ثم يحوّل المستخدم، والتسوية
|
||||||
/// تصل لاحقاً عبر مطابقة رسالة المزوّد. هذا **نفس مسار سيرو** حرفياً.
|
/// تصل لاحقاً عبر مطابقة رسالة المزوّد. هذا **نفس مسار سيرو** حرفياً.
|
||||||
/// PayMob وحده يرجّع رابط إعادة توجيه، و`cash` وحده فوري.
|
/// PayMob وحده يرجّع رابط إعادة توجيه، و`cash` وحده فوري.
|
||||||
/// شحن **الرصيد التشغيلي** للسائق — نيّة مختلفة عن شحن المحفظة:
|
|
||||||
/// `credit_topup` إيرادٌ يُقيَّد على المستأجر، والخادم يرفضها من غير
|
|
||||||
/// السائق (`payments.service.ts:67`).
|
|
||||||
Future<TopupResult> chargeCredit({
|
|
||||||
required num amount,
|
|
||||||
required String provider,
|
|
||||||
required String currency,
|
|
||||||
}) =>
|
|
||||||
charge(
|
|
||||||
amount: amount,
|
|
||||||
provider: provider,
|
|
||||||
currency: currency,
|
|
||||||
purpose: 'credit_topup',
|
|
||||||
);
|
|
||||||
|
|
||||||
Future<TopupResult> charge({
|
Future<TopupResult> charge({
|
||||||
required num amount,
|
required num amount,
|
||||||
required String provider,
|
required String provider,
|
||||||
required String currency,
|
required String currency,
|
||||||
String purpose = 'topup',
|
|
||||||
}) async {
|
}) async {
|
||||||
final res = await _api.post<Map<String, dynamic>>(
|
final res = await _api.post<Map<String, dynamic>>(
|
||||||
'/payments/charge',
|
'/payments/charge',
|
||||||
body: {
|
body: {
|
||||||
'amount': amount,
|
'amount': amount,
|
||||||
'provider': provider,
|
'provider': provider,
|
||||||
'purpose': purpose,
|
'purpose': 'topup',
|
||||||
'currency': currency,
|
'currency': currency,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -877,6 +877,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.0"
|
version: "0.6.0"
|
||||||
|
share_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: share_plus
|
||||||
|
sha256: "223873d106614442ea6f20db5a038685cc5b32a2fba81cdecaefbbae0523f7fa"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "12.0.2"
|
||||||
|
share_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: share_plus_platform_interface
|
||||||
|
sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.0"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1010,6 +1026,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
url_launcher:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: url_launcher
|
||||||
|
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.2"
|
||||||
|
url_launcher_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_android
|
||||||
|
sha256: "17bc677f0b301615530dd1d67e0a9828cafa2d0b6b6eae4cd3679b7eac4a273c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.30"
|
||||||
|
url_launcher_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_ios
|
||||||
|
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.4.1"
|
||||||
|
url_launcher_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_linux
|
||||||
|
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.2"
|
||||||
|
url_launcher_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_macos
|
||||||
|
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.5"
|
||||||
|
url_launcher_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_platform_interface
|
||||||
|
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
|
url_launcher_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_web
|
||||||
|
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.3"
|
||||||
|
url_launcher_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_windows
|
||||||
|
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.5"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ dependencies:
|
|||||||
geolocator: ^14.0.2
|
geolocator: ^14.0.2
|
||||||
socket_io_client: ^3.0.2
|
socket_io_client: ^3.0.2
|
||||||
|
|
||||||
|
# الدراور: المشاركة وفتح الروابط
|
||||||
|
share_plus: ^12.0.1
|
||||||
|
url_launcher: ^6.3.2
|
||||||
|
|
||||||
# م6: الإشعارات — الإعداد الأصلي (google-services) منقول مع الطبقة الأصلية
|
# م6: الإشعارات — الإعداد الأصلي (google-services) منقول مع الطبقة الأصلية
|
||||||
firebase_core: ^4.4.0
|
firebase_core: ^4.4.0
|
||||||
firebase_messaging: ^16.1.1
|
firebase_messaging: ^16.1.1
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ EXTRA = {
|
|||||||
"phoneTitle": "أهلاً بك، كابتن",
|
"phoneTitle": "أهلاً بك، كابتن",
|
||||||
"profileLead": "اسمك يظهر للراكب عند الرحلة.",
|
"profileLead": "اسمك يظهر للراكب عند الرحلة.",
|
||||||
"historyEmpty": "لا رحلات بعد — اتصل لتصلك الطلبات",
|
"historyEmpty": "لا رحلات بعد — اتصل لتصلك الطلبات",
|
||||||
|
"rewardsInviteTitle": "ادعُ سائقاً",
|
||||||
|
"rewardsInviteLead": "شارك كودك — تربح عن كل سائق ينضم",
|
||||||
|
"menuInvite": "ادعُ سائقاً",
|
||||||
|
"menuDriverApp": "تطبيق الراكب",
|
||||||
|
"shareMessage": "انضم لـTripz كسائق — استعمل كودي {code}",
|
||||||
},
|
},
|
||||||
'en': {
|
'en': {
|
||||||
"obApplyTitle": "Register as a driver",
|
"obApplyTitle": "Register as a driver",
|
||||||
@@ -81,6 +86,11 @@ EXTRA = {
|
|||||||
"phoneTitle": "Welcome, captain",
|
"phoneTitle": "Welcome, captain",
|
||||||
"profileLead": "Your name is shown to the rider during a trip.",
|
"profileLead": "Your name is shown to the rider during a trip.",
|
||||||
"historyEmpty": "No trips yet — go online to receive requests",
|
"historyEmpty": "No trips yet — go online to receive requests",
|
||||||
|
"rewardsInviteTitle": "Invite a driver",
|
||||||
|
"rewardsInviteLead": "Share your code — earn for every driver who joins",
|
||||||
|
"menuInvite": "Invite a driver",
|
||||||
|
"menuDriverApp": "Rider app",
|
||||||
|
"shareMessage": "Join Tripz as a driver — use my code {code}",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ cd "$(dirname "$0")/.."
|
|||||||
# ملفات يملكها السائق ولا تُنسخ من الراكب أبداً
|
# ملفات يملكها السائق ولا تُنسخ من الراكب أبداً
|
||||||
OWN=(
|
OWN=(
|
||||||
"core/config.dart" # appRole = driver
|
"core/config.dart" # appRole = driver
|
||||||
"core/build_config.dart" # اسم التطبيق (الألوان تُحدَّث يدوياً معاً)
|
"core/build_config.dart" # اسم التطبيق + روابط الطرف الآخر
|
||||||
"core/di.dart" # DutyCubit + OnboardingCubit + PayoutCubit
|
"core/di.dart" # DutyCubit + OnboardingCubit + PayoutCubit
|
||||||
"core/router.dart" # DutyPage + EarningsPage + بوابة التسجيل
|
"core/router.dart" # DutyPage + EarningsPage + بوابة التسجيل
|
||||||
"core/l10n/gen"
|
"core/l10n/gen"
|
||||||
|
|||||||
@@ -22,6 +22,28 @@ class BuildConfig {
|
|||||||
|
|
||||||
static const String logoAsset = 'assets/images/logo.png';
|
static const String logoAsset = 'assets/images/logo.png';
|
||||||
static const String splashLogoAsset = 'assets/images/splash_logo.png';
|
static const String splashLogoAsset = 'assets/images/splash_logo.png';
|
||||||
|
|
||||||
|
/// روابط خارجية لكل مستأجر — تُولَّد مع بقية الملف (N3).
|
||||||
|
static const String storeUrl = String.fromEnvironment(
|
||||||
|
'STORE_URL',
|
||||||
|
defaultValue: 'https://tripz.app',
|
||||||
|
);
|
||||||
|
static const String privacyUrl = String.fromEnvironment(
|
||||||
|
'PRIVACY_URL',
|
||||||
|
defaultValue: 'https://tripz.app/privacy',
|
||||||
|
);
|
||||||
|
|
||||||
|
/// رقم دعم واتساب المستأجر — بصيغة دولية بلا `+`.
|
||||||
|
static const String supportPhone = String.fromEnvironment(
|
||||||
|
'SUPPORT_PHONE',
|
||||||
|
defaultValue: '962790000000',
|
||||||
|
);
|
||||||
|
|
||||||
|
/// رابط تطبيق الطرف الآخر — الراكب يدعو نفسه ليصير سائقاً والعكس.
|
||||||
|
static const String driverAppUrl = String.fromEnvironment(
|
||||||
|
'DRIVER_APP_URL',
|
||||||
|
defaultValue: 'https://tripz.app/driver',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// أعلام الاستحقاقات (docs/23 §5). **`const` إجبارياً** كي يُحذف كود الميزة
|
/// أعلام الاستحقاقات (docs/23 §5). **`const` إجبارياً** كي يُحذف كود الميزة
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import '../features/chat/data/chat_repository.dart';
|
|||||||
import '../features/profile/data/profile_repository.dart';
|
import '../features/profile/data/profile_repository.dart';
|
||||||
import '../features/rating/cubit/rating_cubit.dart';
|
import '../features/rating/cubit/rating_cubit.dart';
|
||||||
import '../features/rating/data/rating_repository.dart';
|
import '../features/rating/data/rating_repository.dart';
|
||||||
|
import '../features/rewards/cubit/rewards_cubit.dart';
|
||||||
|
import '../features/rewards/data/rewards_repository.dart';
|
||||||
import '../features/settings/cubit/settings_cubit.dart';
|
import '../features/settings/cubit/settings_cubit.dart';
|
||||||
import '../features/trip/cubit/history_cubit.dart';
|
import '../features/trip/cubit/history_cubit.dart';
|
||||||
import '../features/trip/cubit/ride_cubit.dart';
|
import '../features/trip/cubit/ride_cubit.dart';
|
||||||
@@ -83,6 +85,12 @@ Future<void> setupInjector() async {
|
|||||||
sl.registerFactory<HistoryCubit>(() => HistoryCubit(sl()));
|
sl.registerFactory<HistoryCubit>(() => HistoryCubit(sl()));
|
||||||
|
|
||||||
sl.registerSingleton<RatingRepository>(RatingRepository(sl()));
|
sl.registerSingleton<RatingRepository>(RatingRepository(sl()));
|
||||||
|
|
||||||
|
// المكافآت خلف علم الكوبونات: الدعوات والكوبونات وجهان لنفس الوحدة.
|
||||||
|
if (Features.coupons) {
|
||||||
|
sl.registerSingleton<RewardsRepository>(RewardsRepository(sl()));
|
||||||
|
sl.registerFactory<RewardsCubit>(() => RewardsCubit(sl()));
|
||||||
|
}
|
||||||
sl.registerFactory<RatingCubit>(() => RatingCubit(sl()));
|
sl.registerFactory<RatingCubit>(() => RatingCubit(sl()));
|
||||||
|
|
||||||
// الدردشة خلف علمها: إطفاؤه يحذف كودها من الـbinary (docs/23 §5).
|
// الدردشة خلف علمها: إطفاؤه يحذف كودها من الـbinary (docs/23 §5).
|
||||||
|
|||||||
@@ -208,5 +208,40 @@
|
|||||||
"walletInvoicePending": "سيُضاف الرصيد فور تأكيد التحويل",
|
"walletInvoicePending": "سيُضاف الرصيد فور تأكيد التحويل",
|
||||||
"walletCopy": "نسخ",
|
"walletCopy": "نسخ",
|
||||||
"walletCopied": "نُسخ",
|
"walletCopied": "نُسخ",
|
||||||
"walletNoTarget": "لم يُضبط حساب استلام لهذه القناة بعد — راجع المشغّل"
|
"walletNoTarget": "لم يُضبط حساب استلام لهذه القناة بعد — راجع المشغّل",
|
||||||
|
"rewardsTitle": "مكافآتي",
|
||||||
|
"rewardsInviteTitle": "ادعُ صديقاً",
|
||||||
|
"rewardsInviteLead": "شارك كودك — تربح أنت وصديقك",
|
||||||
|
"rewardsCode": "كود الدعوة",
|
||||||
|
"rewardsInvited": "دعوتَهم",
|
||||||
|
"rewardsPending": "قيد التأكيد",
|
||||||
|
"rewardsPaid": "مكافآت مدفوعة",
|
||||||
|
"rewardsCoupons": "كوبوناتي",
|
||||||
|
"rewardsCouponsEmpty": "لا كوبونات صالحة الآن",
|
||||||
|
"menuRewards": "مكافآتي",
|
||||||
|
"menuInvite": "ادعُ صديقاً",
|
||||||
|
"menuShare": "شارك التطبيق",
|
||||||
|
"menuNotifications": "الإشعارات",
|
||||||
|
"menuPrivacy": "سياسة الخصوصية",
|
||||||
|
"menuRateApp": "قيّم التطبيق",
|
||||||
|
"shareMessage": "جرّب تطبيق Tripz — استعمل كودي {code} واحصل على خصم أول رحلة",
|
||||||
|
"menuDriverApp": "سجّل كسائق",
|
||||||
|
"@shareMessage": {
|
||||||
|
"placeholders": {
|
||||||
|
"code": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"supportFaqQ1": "كيف أطلب رحلة؟",
|
||||||
|
"supportFaqA1": "حدّد وجهتك من البحث أو من الخريطة، اختر نوع الرحلة، ثم اضغط «اطلب الرحلة».",
|
||||||
|
"supportFaqQ2": "كيف أشحن رصيدي؟",
|
||||||
|
"supportFaqA2": "من المحفظة اختر المبلغ وطريقة الدفع. ستظهر لك تعليمات التحويل ورقم مرجع — يُضاف الرصيد بعد تأكيد التحويل.",
|
||||||
|
"supportFaqQ3": "لماذا لا يظهر سائق؟",
|
||||||
|
"supportFaqA3": "قد لا يوجد سائقون قريبون في هذا الوقت. جرّب بعد قليل أو غيّر نوع الرحلة.",
|
||||||
|
"supportFaqQ4": "كيف ألغي رحلة؟",
|
||||||
|
"supportFaqA4": "من بطاقة الرحلة اضغط «إلغاء الرحلة». الإلغاء بعد وصول السائق قد يترتّب عليه رسم.",
|
||||||
|
"supportDeviceCheck": "فحص توافق الجهاز",
|
||||||
|
"supportDeviceOk": "جهازك متوافق",
|
||||||
|
"supportDeviceIssues": "مشاكل قد تؤثّر على عمل التطبيق"
|
||||||
}
|
}
|
||||||
@@ -208,5 +208,40 @@
|
|||||||
"walletInvoicePending": "Your balance updates once the transfer is confirmed",
|
"walletInvoicePending": "Your balance updates once the transfer is confirmed",
|
||||||
"walletCopy": "Copy",
|
"walletCopy": "Copy",
|
||||||
"walletCopied": "Copied",
|
"walletCopied": "Copied",
|
||||||
"walletNoTarget": "No receiving account is configured for this channel yet — contact the operator"
|
"walletNoTarget": "No receiving account is configured for this channel yet — contact the operator",
|
||||||
|
"rewardsTitle": "My rewards",
|
||||||
|
"rewardsInviteTitle": "Invite a friend",
|
||||||
|
"rewardsInviteLead": "Share your code — you both earn",
|
||||||
|
"rewardsCode": "Invite code",
|
||||||
|
"rewardsInvited": "Invited",
|
||||||
|
"rewardsPending": "Pending",
|
||||||
|
"rewardsPaid": "Rewards paid",
|
||||||
|
"rewardsCoupons": "My coupons",
|
||||||
|
"rewardsCouponsEmpty": "No valid coupons right now",
|
||||||
|
"menuRewards": "My rewards",
|
||||||
|
"menuInvite": "Invite a friend",
|
||||||
|
"menuShare": "Share the app",
|
||||||
|
"menuNotifications": "Notifications",
|
||||||
|
"menuPrivacy": "Privacy policy",
|
||||||
|
"menuRateApp": "Rate the app",
|
||||||
|
"shareMessage": "Try Tripz — use my code {code} for a discount on your first ride",
|
||||||
|
"menuDriverApp": "Become a driver",
|
||||||
|
"@shareMessage": {
|
||||||
|
"placeholders": {
|
||||||
|
"code": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"supportFaqQ1": "How do I request a ride?",
|
||||||
|
"supportFaqA1": "Pick your destination from search or the map, choose a ride type, then tap “Request ride”.",
|
||||||
|
"supportFaqQ2": "How do I top up?",
|
||||||
|
"supportFaqA2": "In the wallet, choose an amount and a payment method. You'll get transfer instructions and a reference — your balance updates once the transfer is confirmed.",
|
||||||
|
"supportFaqQ3": "Why is no driver showing up?",
|
||||||
|
"supportFaqA3": "There may be no drivers nearby right now. Try again shortly or change the ride type.",
|
||||||
|
"supportFaqQ4": "How do I cancel?",
|
||||||
|
"supportFaqA4": "Tap “Cancel ride” on the trip card. Cancelling after the driver arrives may incur a fee.",
|
||||||
|
"supportDeviceCheck": "Check device compatibility",
|
||||||
|
"supportDeviceOk": "Your device is compatible",
|
||||||
|
"supportDeviceIssues": "Issues that may affect the app"
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ import '../features/chat/view/chat_page.dart';
|
|||||||
import '../features/profile/view/profile_edit_page.dart';
|
import '../features/profile/view/profile_edit_page.dart';
|
||||||
import '../features/profile/view/profile_setup_page.dart';
|
import '../features/profile/view/profile_setup_page.dart';
|
||||||
import '../features/rating/data/rating_repository.dart';
|
import '../features/rating/data/rating_repository.dart';
|
||||||
|
import '../features/rewards/view/rewards_page.dart';
|
||||||
import '../features/rating/view/rating_page.dart';
|
import '../features/rating/view/rating_page.dart';
|
||||||
import '../features/settings/view/settings_page.dart';
|
import '../features/settings/view/settings_page.dart';
|
||||||
import '../features/splash/view/splash_page.dart';
|
import '../features/splash/view/splash_page.dart';
|
||||||
@@ -31,6 +32,7 @@ class Routes {
|
|||||||
static const profileEdit = '/account';
|
static const profileEdit = '/account';
|
||||||
static const settings = '/settings';
|
static const settings = '/settings';
|
||||||
static const support = '/support';
|
static const support = '/support';
|
||||||
|
static const rewards = '/rewards';
|
||||||
static const chat = '/chat';
|
static const chat = '/chat';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +112,10 @@ final appRouter = GoRouter(
|
|||||||
builder: (context, state) =>
|
builder: (context, state) =>
|
||||||
ChatPage(tripId: state.uri.queryParameters['trip'] ?? ''),
|
ChatPage(tripId: state.uri.queryParameters['trip'] ?? ''),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.rewards,
|
||||||
|
builder: (context, state) => const RewardsPage(),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: Routes.support,
|
path: Routes.support,
|
||||||
builder: (context, state) => const SupportPage(),
|
builder: (context, state) => const SupportPage(),
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../build_config.dart';
|
import '../build_config.dart';
|
||||||
import '../config.dart';
|
import '../config.dart';
|
||||||
@@ -52,6 +54,13 @@ class AppDrawer extends StatelessWidget {
|
|||||||
label: isDriver ? l10n.menuEarnings : l10n.menuWallet,
|
label: isDriver ? l10n.menuEarnings : l10n.menuWallet,
|
||||||
route: Routes.wallet,
|
route: Routes.wallet,
|
||||||
),
|
),
|
||||||
|
if (Features.coupons && context.tenantHas('coupons'))
|
||||||
|
_Item(
|
||||||
|
icon: Icons.card_giftcard_rounded,
|
||||||
|
label: l10n.menuRewards,
|
||||||
|
route: Routes.rewards,
|
||||||
|
),
|
||||||
|
const Divider(indent: Space.md, endIndent: Space.md),
|
||||||
_Item(
|
_Item(
|
||||||
icon: Icons.settings_outlined,
|
icon: Icons.settings_outlined,
|
||||||
label: l10n.menuSettings,
|
label: l10n.menuSettings,
|
||||||
@@ -62,6 +71,29 @@ class AppDrawer extends StatelessWidget {
|
|||||||
label: l10n.menuSupport,
|
label: l10n.menuSupport,
|
||||||
route: Routes.support,
|
route: Routes.support,
|
||||||
),
|
),
|
||||||
|
// مداخل محليّة بلا نقطة خادم — تُنفَّذ في الجهاز مباشرة.
|
||||||
|
_Action(
|
||||||
|
icon: Icons.share_outlined,
|
||||||
|
label: l10n.menuShare,
|
||||||
|
onTap: () => SharePlus.instance
|
||||||
|
.share(ShareParams(text: l10n.shareMessage(''))),
|
||||||
|
),
|
||||||
|
_Action(
|
||||||
|
icon: Icons.star_outline_rounded,
|
||||||
|
label: l10n.menuRateApp,
|
||||||
|
onTap: () => _open(BuildConfig.storeUrl),
|
||||||
|
),
|
||||||
|
_Action(
|
||||||
|
icon: Icons.shield_outlined,
|
||||||
|
label: l10n.menuPrivacy,
|
||||||
|
onTap: () => _open(BuildConfig.privacyUrl),
|
||||||
|
),
|
||||||
|
if (!isDriver)
|
||||||
|
_Action(
|
||||||
|
icon: Icons.local_taxi_outlined,
|
||||||
|
label: l10n.menuDriverApp,
|
||||||
|
onTap: () => _open(BuildConfig.driverAppUrl),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -83,6 +115,13 @@ class AppDrawer extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<void> _open(String url) async {
|
||||||
|
final uri = Uri.tryParse(url);
|
||||||
|
if (uri == null) return;
|
||||||
|
// فشل الفتح لا يُسقط شيئاً — رابط خارجي ليس جزءاً من التطبيق.
|
||||||
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _confirmLogout(BuildContext context) async {
|
Future<void> _confirmLogout(BuildContext context) async {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
final session = context.read<SessionCubit>();
|
final session = context.read<SessionCubit>();
|
||||||
@@ -155,6 +194,31 @@ class _Header extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// مدخل ينفّذ فعلاً محلياً (مشاركة · رابط خارجي) لا تنقّلاً.
|
||||||
|
class _Action extends StatelessWidget {
|
||||||
|
const _Action({
|
||||||
|
required this.icon,
|
||||||
|
required this.label,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(icon),
|
||||||
|
title: Text(label),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
onTap();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _Item extends StatelessWidget {
|
class _Item extends StatelessWidget {
|
||||||
const _Item({required this.icon, required this.label, required this.route});
|
const _Item({required this.icon, required this.label, required this.route});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
import '../../../core/api/api_exception.dart';
|
||||||
|
import '../../../core/ui/view_status.dart';
|
||||||
|
import '../data/rewards_repository.dart';
|
||||||
|
|
||||||
|
class RewardsState {
|
||||||
|
const RewardsState({
|
||||||
|
this.status = ViewStatus.idle,
|
||||||
|
this.referrals,
|
||||||
|
this.coupons = const [],
|
||||||
|
this.error,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ViewStatus status;
|
||||||
|
final ReferralSummary? referrals;
|
||||||
|
final List<Coupon> coupons;
|
||||||
|
final String? error;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RewardsCubit extends Cubit<RewardsState> {
|
||||||
|
RewardsCubit(this._repo) : super(const RewardsState());
|
||||||
|
|
||||||
|
final RewardsRepository _repo;
|
||||||
|
|
||||||
|
Future<void> load() async {
|
||||||
|
emit(const RewardsState(status: ViewStatus.loading));
|
||||||
|
try {
|
||||||
|
// النداءان متوازيان: لا سبب لانتظار أحدهما الآخر.
|
||||||
|
final results = await Future.wait([
|
||||||
|
_repo.referrals(),
|
||||||
|
_repo.coupons(),
|
||||||
|
]);
|
||||||
|
if (isClosed) return;
|
||||||
|
emit(RewardsState(
|
||||||
|
status: ViewStatus.success,
|
||||||
|
referrals: results[0] as ReferralSummary,
|
||||||
|
coupons: results[1] as List<Coupon>,
|
||||||
|
));
|
||||||
|
} on ApiException catch (e) {
|
||||||
|
if (!isClosed) {
|
||||||
|
emit(RewardsState(status: ViewStatus.error, error: e.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import '../../../core/api/api_client.dart';
|
||||||
|
|
||||||
|
/// ملخّص الدعوات — شكل مُتحقَّق حيّاً: `{code, total, pending, paid}`.
|
||||||
|
class ReferralSummary {
|
||||||
|
const ReferralSummary({
|
||||||
|
required this.code,
|
||||||
|
required this.total,
|
||||||
|
required this.pending,
|
||||||
|
required this.paid,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// كود دعوتي الذي أشاركه.
|
||||||
|
final String code;
|
||||||
|
|
||||||
|
final int total;
|
||||||
|
final int pending;
|
||||||
|
final int paid;
|
||||||
|
|
||||||
|
factory ReferralSummary.fromJson(Map<String, dynamic> json) =>
|
||||||
|
ReferralSummary(
|
||||||
|
code: json['code'] as String? ?? '',
|
||||||
|
total: (json['total'] as num?)?.toInt() ?? 0,
|
||||||
|
pending: (json['pending'] as num?)?.toInt() ?? 0,
|
||||||
|
paid: (json['paid'] as num?)?.toInt() ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coupon {
|
||||||
|
const Coupon({
|
||||||
|
required this.code,
|
||||||
|
this.description = '',
|
||||||
|
this.expiresAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String code;
|
||||||
|
final String description;
|
||||||
|
final DateTime? expiresAt;
|
||||||
|
|
||||||
|
factory Coupon.fromJson(Map<String, dynamic> json) => Coupon(
|
||||||
|
code: json['code'] as String? ?? '',
|
||||||
|
description: json['description'] as String? ??
|
||||||
|
json['title'] as String? ??
|
||||||
|
'',
|
||||||
|
expiresAt: DateTime.tryParse(json['expires_at']?.toString() ?? ''),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class RewardsRepository {
|
||||||
|
RewardsRepository(this._api);
|
||||||
|
|
||||||
|
final ApiClient _api;
|
||||||
|
|
||||||
|
Future<ReferralSummary> referrals() async {
|
||||||
|
final res = await _api.get<Map<String, dynamic>>('/rewards/referrals');
|
||||||
|
return ReferralSummary.fromJson(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Coupon>> coupons() async {
|
||||||
|
final res = await _api.get<List<dynamic>>('/rewards/coupons');
|
||||||
|
return res
|
||||||
|
.whereType<Map<String, dynamic>>()
|
||||||
|
.map(Coupon.fromJson)
|
||||||
|
.toList(growable: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
|
import '../../../core/design/tokens.dart';
|
||||||
|
import '../../../core/design/tripz_colors.dart';
|
||||||
|
import '../../../core/design/typography.dart';
|
||||||
|
import '../../../core/di.dart';
|
||||||
|
import '../../../core/l10n/l10n.dart';
|
||||||
|
import '../../../core/ui/tripz_button.dart';
|
||||||
|
import '../../../core/ui/tripz_card.dart';
|
||||||
|
import '../../../core/ui/tripz_scaffold.dart';
|
||||||
|
import '../cubit/rewards_cubit.dart';
|
||||||
|
|
||||||
|
/// «مكافآتي» — الدعوات والكوبونات في شاشة واحدة (نمط سيرو: Promos + Invite).
|
||||||
|
class RewardsPage extends StatefulWidget {
|
||||||
|
const RewardsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RewardsPage> createState() => _RewardsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RewardsPageState extends State<RewardsPage> {
|
||||||
|
late final RewardsCubit _cubit = sl<RewardsCubit>()..load();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_cubit.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return BlocProvider.value(
|
||||||
|
value: _cubit,
|
||||||
|
child: BlocBuilder<RewardsCubit, RewardsState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final ref = state.referrals;
|
||||||
|
return TripzScaffold(
|
||||||
|
title: l10n.rewardsTitle,
|
||||||
|
status: state.status,
|
||||||
|
error: state.error,
|
||||||
|
onRetry: _cubit.load,
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
if (ref != null) _InviteCard(code: ref.code, summary: ref),
|
||||||
|
const SizedBox(height: Space.lg),
|
||||||
|
Text(l10n.rewardsCoupons, style: context.texts.titleMedium),
|
||||||
|
const SizedBox(height: Space.xs),
|
||||||
|
if (state.coupons.isEmpty)
|
||||||
|
Text(
|
||||||
|
l10n.rewardsCouponsEmpty,
|
||||||
|
style: context.texts.bodySmall?.copyWith(
|
||||||
|
color: context.colors.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
for (final coupon in state.coupons) ...[
|
||||||
|
TripzCard(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.local_offer_outlined,
|
||||||
|
color: context.tripzColors.success,
|
||||||
|
),
|
||||||
|
const SizedBox(width: Space.sm),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
coupon.code,
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
style: numericStyle(context.texts.titleSmall),
|
||||||
|
),
|
||||||
|
if (coupon.description.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
coupon.description,
|
||||||
|
style: context.texts.bodySmall?.copyWith(
|
||||||
|
color: context.colors.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: Space.xs),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InviteCard extends StatelessWidget {
|
||||||
|
const _InviteCard({required this.code, required this.summary});
|
||||||
|
|
||||||
|
final String code;
|
||||||
|
final dynamic summary;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return Container(
|
||||||
|
padding: Space.page,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.colors.primaryContainer,
|
||||||
|
borderRadius: Radii.card_,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.rewardsInviteTitle,
|
||||||
|
style: context.texts.titleMedium?.copyWith(
|
||||||
|
color: context.colors.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: Space.xxs),
|
||||||
|
Text(
|
||||||
|
l10n.rewardsInviteLead,
|
||||||
|
style: context.texts.bodySmall?.copyWith(
|
||||||
|
color: context.colors.onPrimaryContainer.withValues(alpha: 0.8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: Space.md),
|
||||||
|
// الكود يُنسخ ويُشارك — الطريقان اللذان يستعملهما الناس فعلاً.
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SelectableText(
|
||||||
|
code,
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
style: numericStyle(context.texts.headlineSmall).copyWith(
|
||||||
|
letterSpacing: 3,
|
||||||
|
color: context.colors.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.copy_rounded),
|
||||||
|
color: context.colors.onPrimaryContainer,
|
||||||
|
onPressed: () => Clipboard.setData(ClipboardData(text: code)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: Space.sm),
|
||||||
|
TripzButton.primary(
|
||||||
|
label: l10n.menuShare,
|
||||||
|
icon: Icons.share_outlined,
|
||||||
|
onPressed: () =>
|
||||||
|
SharePlus.instance.share(ShareParams(text: l10n.shareMessage(code))),
|
||||||
|
),
|
||||||
|
const SizedBox(height: Space.md),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
_Stat(label: l10n.rewardsInvited, value: summary.total),
|
||||||
|
_Stat(label: l10n.rewardsPending, value: summary.pending),
|
||||||
|
_Stat(label: l10n.rewardsPaid, value: summary.paid),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Stat extends StatelessWidget {
|
||||||
|
const _Stat({required this.label, required this.value});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final int value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'$value',
|
||||||
|
style: numericStyle(context.texts.titleLarge).copyWith(
|
||||||
|
color: context.colors.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: context.texts.labelSmall?.copyWith(
|
||||||
|
color: context.colors.onPrimaryContainer.withValues(alpha: 0.8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../../../core/build_config.dart';
|
import '../../../core/build_config.dart';
|
||||||
import '../../../core/design/tokens.dart';
|
import '../../../core/design/tokens.dart';
|
||||||
@@ -7,35 +10,79 @@ import '../../../core/l10n/l10n.dart';
|
|||||||
import '../../../core/ui/tripz_card.dart';
|
import '../../../core/ui/tripz_card.dart';
|
||||||
import '../../../core/ui/tripz_scaffold.dart';
|
import '../../../core/ui/tripz_scaffold.dart';
|
||||||
|
|
||||||
/// المساعدة و«عن التطبيق». محتوى محلّي عمداً: الخادم لا يوفّر نقطة أسئلة
|
/// المساعدة: أسئلة شائعة · تواصل · فحص توافق الجهاز · عن التطبيق.
|
||||||
/// شائعة، وربطها بنصّ من الشبكة يعني شاشة فارغة حين ينقطع الاتصال — وهو
|
///
|
||||||
/// بالضبط الوقت الذي يفتح فيه المستخدم المساعدة.
|
/// المحتوى **محلّي عمداً**: الخادم لا يوفّر نقطة أسئلة شائعة، وربطها بالشبكة
|
||||||
|
/// يعني شاشة فارغة حين ينقطع الاتصال — وهو بالضبط الوقت الذي يفتح فيه
|
||||||
|
/// المستخدم المساعدة.
|
||||||
class SupportPage extends StatelessWidget {
|
class SupportPage extends StatelessWidget {
|
||||||
const SupportPage({super.key});
|
const SupportPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
|
final faq = [
|
||||||
|
(l10n.supportFaqQ1, l10n.supportFaqA1),
|
||||||
|
(l10n.supportFaqQ2, l10n.supportFaqA2),
|
||||||
|
(l10n.supportFaqQ3, l10n.supportFaqA3),
|
||||||
|
(l10n.supportFaqQ4, l10n.supportFaqA4),
|
||||||
|
];
|
||||||
|
|
||||||
return TripzScaffold(
|
return TripzScaffold(
|
||||||
title: l10n.supportTitle,
|
title: l10n.supportTitle,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
TripzCard(
|
Text(l10n.supportFaq, style: context.texts.titleMedium),
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(l10n.supportContact, style: context.texts.titleMedium),
|
|
||||||
const SizedBox(height: Space.xs),
|
const SizedBox(height: Space.xs),
|
||||||
Text(
|
for (final (q, a) in faq)
|
||||||
l10n.supportWhatsapp,
|
Card(
|
||||||
style: context.texts.bodyMedium?.copyWith(
|
margin: const EdgeInsets.only(bottom: Space.xs),
|
||||||
|
child: ExpansionTile(
|
||||||
|
shape: const RoundedRectangleBorder(borderRadius: Radii.card_),
|
||||||
|
title: Text(q, style: context.texts.bodyMedium),
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: Space.page,
|
||||||
|
child: Text(
|
||||||
|
a,
|
||||||
|
style: context.texts.bodySmall?.copyWith(
|
||||||
color: context.colors.onSurfaceVariant,
|
color: context.colors.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: Space.lg),
|
||||||
|
Text(l10n.supportContact, style: context.texts.titleMedium),
|
||||||
|
const SizedBox(height: Space.xs),
|
||||||
|
TripzCard(
|
||||||
|
onTap: () => _openWhatsapp(BuildConfig.supportPhone),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.chat_outlined, color: context.tripzColors.success),
|
||||||
|
const SizedBox(width: Space.sm),
|
||||||
|
Expanded(child: Text(l10n.supportWhatsapp)),
|
||||||
|
const Icon(Icons.chevron_left_rounded),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: Space.md),
|
const SizedBox(height: Space.md),
|
||||||
|
TripzCard(
|
||||||
|
onTap: () => _showDeviceCheck(context),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.phonelink_setup_outlined),
|
||||||
|
const SizedBox(width: Space.sm),
|
||||||
|
Expanded(child: Text(l10n.supportDeviceCheck)),
|
||||||
|
const Icon(Icons.chevron_left_rounded),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: Space.lg),
|
||||||
TripzCard(
|
TripzCard(
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
@@ -54,4 +101,69 @@ class SupportPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<void> _openWhatsapp(String phone) async {
|
||||||
|
final uri = Uri.parse('https://wa.me/$phone');
|
||||||
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// فحص توافق الجهاز — نمط سيرو. يفحص ما يُعطّل التطبيق فعلاً: خدمة الموقع
|
||||||
|
/// وإذنها وإذن الإشعارات. أسبابٌ حقيقية لشكاوى «التطبيق لا يعمل».
|
||||||
|
static Future<void> _showDeviceCheck(BuildContext context) async {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final issues = <String>[];
|
||||||
|
|
||||||
|
if (!await Geolocator.isLocationServiceEnabled()) {
|
||||||
|
issues.add(l10n.rideNoLocation);
|
||||||
|
}
|
||||||
|
final locationPermission = await Permission.locationWhenInUse.status;
|
||||||
|
if (!locationPermission.isGranted && !locationPermission.isLimited) {
|
||||||
|
issues.add(l10n.permissionDeniedForever);
|
||||||
|
}
|
||||||
|
if (await Permission.notification.isDenied) {
|
||||||
|
issues.add(l10n.notifDenied);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.mounted) return;
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(l10n.supportDeviceCheck),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: issues.isEmpty
|
||||||
|
? [
|
||||||
|
Row(children: [
|
||||||
|
Icon(Icons.check_circle_rounded,
|
||||||
|
color: context.tripzColors.success),
|
||||||
|
const SizedBox(width: Space.xs),
|
||||||
|
Expanded(child: Text(l10n.supportDeviceOk)),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
Text(l10n.supportDeviceIssues),
|
||||||
|
const SizedBox(height: Space.xs),
|
||||||
|
for (final issue in issues)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: Space.xxs),
|
||||||
|
child: Row(children: [
|
||||||
|
Icon(Icons.error_outline_rounded,
|
||||||
|
size: Sizes.iconSm,
|
||||||
|
color: context.tripzColors.warning),
|
||||||
|
const SizedBox(width: Space.xs),
|
||||||
|
Expanded(child: Text(issue)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(l10n.actionContinue),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
version: "1.19.1"
|
||||||
|
cross_file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cross_file
|
||||||
|
sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.5+4"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -765,6 +773,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.0"
|
version: "0.6.0"
|
||||||
|
share_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: share_plus
|
||||||
|
sha256: "223873d106614442ea6f20db5a038685cc5b32a2fba81cdecaefbbae0523f7fa"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "12.0.2"
|
||||||
|
share_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: share_plus_platform_interface
|
||||||
|
sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.0"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -898,6 +922,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
url_launcher:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: url_launcher
|
||||||
|
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.2"
|
||||||
|
url_launcher_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_android
|
||||||
|
sha256: "17bc677f0b301615530dd1d67e0a9828cafa2d0b6b6eae4cd3679b7eac4a273c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.30"
|
||||||
|
url_launcher_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_ios
|
||||||
|
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.4.1"
|
||||||
|
url_launcher_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_linux
|
||||||
|
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.2"
|
||||||
|
url_launcher_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_macos
|
||||||
|
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.5"
|
||||||
|
url_launcher_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_platform_interface
|
||||||
|
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
|
url_launcher_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_web
|
||||||
|
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.3"
|
||||||
|
url_launcher_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_windows
|
||||||
|
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.5"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ dependencies:
|
|||||||
geolocator: ^14.0.2
|
geolocator: ^14.0.2
|
||||||
socket_io_client: ^3.0.2
|
socket_io_client: ^3.0.2
|
||||||
|
|
||||||
|
# الدراور: المشاركة وفتح الروابط
|
||||||
|
share_plus: ^12.0.1
|
||||||
|
url_launcher: ^6.3.2
|
||||||
|
|
||||||
# م6: الإشعارات — الإعداد الأصلي (google-services) منقول مع الطبقة الأصلية
|
# م6: الإشعارات — الإعداد الأصلي (google-services) منقول مع الطبقة الأصلية
|
||||||
firebase_core: ^4.4.0
|
firebase_core: ^4.4.0
|
||||||
firebase_messaging: ^16.1.1
|
firebase_messaging: ^16.1.1
|
||||||
|
|||||||
Reference in New Issue
Block a user