Food delivery driver module + masked calls + TURN
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
// food_notification_service.dart — إشعارات وحدة التوصيل، منفصلة كلياً عن إشعارات الرحلات
|
||||
//
|
||||
// الفصل مقصود على ثلاثة مستويات:
|
||||
// 1) قناتان أندرويد خاصتان (food_delivery_offer_channel / food_delivery_status_channel)
|
||||
// غير 'high_importance_channel' الخاص بالرحلات — فيقدر السائق يكتم أو يغيّر
|
||||
// نغمة التوصيل من إعدادات النظام دون أن يمسّ تنبيه الرحلات إطلاقاً.
|
||||
// 2) معرّفات إشعارات مستقلة (7001/7002) فلا يستبدل إشعار توصيل إشعارَ رحلة.
|
||||
// 3) أزرار وحمولة خاصة ('FOOD_ACCEPT'/'FOOD_REJECT' + {"food_event": ...})
|
||||
// يلتقطها فرع مستقل في handleNotificationResponse.
|
||||
import 'dart:convert';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
|
||||
import 'food_delivery_models.dart';
|
||||
|
||||
class FoodNotificationService {
|
||||
FoodNotificationService._();
|
||||
static final FoodNotificationService instance = FoodNotificationService._();
|
||||
|
||||
// نفس نسخة الإضافة المستخدمة في NotificationController (singleton داخل الحزمة)،
|
||||
// لكن بقنوات ومعرّفات خاصة بنا — لا نستدعي initialize هنا إطلاقاً حتى لا
|
||||
// نستبدل معالِج النقر العام الذي هيّأه تطبيق الرحلات عند الإقلاع.
|
||||
final FlutterLocalNotificationsPlugin _plugin = FlutterLocalNotificationsPlugin();
|
||||
|
||||
static const String offerChannelId = 'food_delivery_offer_channel';
|
||||
static const String statusChannelId = 'food_delivery_status_channel';
|
||||
static const int offerNotificationId = 7001;
|
||||
static const int statusNotificationId = 7002;
|
||||
|
||||
bool _channelsReady = false;
|
||||
|
||||
Future<void> ensureChannels() async {
|
||||
if (_channelsReady) return;
|
||||
|
||||
final android = _plugin.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>();
|
||||
if (android == null) {
|
||||
_channelsReady = true; // iOS — لا قنوات
|
||||
return;
|
||||
}
|
||||
|
||||
const offerChannel = AndroidNotificationChannel(
|
||||
offerChannelId,
|
||||
'عروض التوصيل',
|
||||
description: 'تنبيه وصول عرض توصيل طلب طعام جديد',
|
||||
importance: Importance.max,
|
||||
playSound: true,
|
||||
sound: RawResourceAndroidNotificationSound('order1'),
|
||||
enableVibration: true,
|
||||
);
|
||||
|
||||
const statusChannel = AndroidNotificationChannel(
|
||||
statusChannelId,
|
||||
'تحديثات طلبات التوصيل',
|
||||
description: 'تغيّر حالة طلب توصيل قائم (جاهز، ملغى، إلخ)',
|
||||
importance: Importance.high,
|
||||
playSound: true,
|
||||
);
|
||||
|
||||
await android.createNotificationChannel(offerChannel);
|
||||
await android.createNotificationChannel(statusChannel);
|
||||
_channelsReady = true;
|
||||
}
|
||||
|
||||
/// إشعار عرض توصيل — ملء الشاشة (fullScreenIntent) كإشعار المكالمة، لأن مهلة
|
||||
/// العرض 20 ثانية ولا تحتمل أن يمرّ السائق على شريط الإشعارات لاحقاً.
|
||||
Future<void> showOfferNotification(FoodDeliveryOffer offer) async {
|
||||
await ensureChannels();
|
||||
|
||||
final body = '${offer.merchantNameAr}\n'
|
||||
'💰 أجرة التوصيل: ${foodFormatPrice(offer.deliveryFee)}'
|
||||
'${offer.itemsCount > 0 ? ' | 🧾 ${offer.itemsCount} صنف' : ''}'
|
||||
'${offer.cashToCollect > 0 ? '\n💵 تحصيل نقدي: ${foodFormatPrice(offer.cashToCollect)}' : ''}';
|
||||
|
||||
final androidDetails = AndroidNotificationDetails(
|
||||
offerChannelId,
|
||||
'عروض التوصيل',
|
||||
importance: Importance.max,
|
||||
priority: Priority.max,
|
||||
fullScreenIntent: true,
|
||||
category: AndroidNotificationCategory.call,
|
||||
visibility: NotificationVisibility.public,
|
||||
timeoutAfter: 20000, // يختفي مع انتهاء مهلة العرض نفسها
|
||||
sound: const RawResourceAndroidNotificationSound('order1'),
|
||||
audioAttributesUsage: AudioAttributesUsage.alarm,
|
||||
color: const Color(0xFFFF6B35),
|
||||
styleInformation: BigTextStyleInformation(
|
||||
body,
|
||||
contentTitle: '🍔 عرض توصيل جديد',
|
||||
summaryText: foodFormatPrice(offer.deliveryFee),
|
||||
),
|
||||
actions: const <AndroidNotificationAction>[
|
||||
AndroidNotificationAction('FOOD_ACCEPT', '✅ قبول',
|
||||
showsUserInterface: true, titleColor: Color(0xFF4CAF50)),
|
||||
AndroidNotificationAction('FOOD_REJECT', '❌ رفض',
|
||||
cancelNotification: true, titleColor: Color(0xFFE53935)),
|
||||
],
|
||||
);
|
||||
|
||||
const iosDetails = DarwinNotificationDetails(
|
||||
sound: 'order1.wav',
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
categoryIdentifier: 'FOOD_OFFER_CATEGORY',
|
||||
interruptionLevel: InterruptionLevel.timeSensitive,
|
||||
);
|
||||
|
||||
await _plugin.show(
|
||||
id: offerNotificationId,
|
||||
title: '🍔 عرض توصيل جديد',
|
||||
body: '${offer.merchantNameAr} — ${foodFormatPrice(offer.deliveryFee)}',
|
||||
notificationDetails:
|
||||
NotificationDetails(android: androidDetails, iOS: iosDetails),
|
||||
payload: jsonEncode({
|
||||
'food_event': 'offer',
|
||||
'order_id': offer.orderId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> cancelOfferNotification() async {
|
||||
await _plugin.cancel(id: offerNotificationId);
|
||||
}
|
||||
|
||||
/// تحديث حالة مهمة قائمة (مثلاً إلغاء من النظام) — قناة أهدأ، بلا ملء شاشة
|
||||
Future<void> showStatusNotification({
|
||||
required int orderId,
|
||||
required String title,
|
||||
required String body,
|
||||
}) async {
|
||||
await ensureChannels();
|
||||
|
||||
const androidDetails = AndroidNotificationDetails(
|
||||
statusChannelId,
|
||||
'تحديثات طلبات التوصيل',
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
color: Color(0xFFFF6B35),
|
||||
);
|
||||
const iosDetails = DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentSound: true,
|
||||
);
|
||||
|
||||
await _plugin.show(
|
||||
id: statusNotificationId,
|
||||
title: title,
|
||||
body: body,
|
||||
notificationDetails:
|
||||
const NotificationDetails(android: androidDetails, iOS: iosDetails),
|
||||
payload: jsonEncode({'food_event': 'status', 'order_id': orderId}),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user