157 lines
6.3 KiB
Dart
157 lines
6.3 KiB
Dart
// 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}),
|
|
);
|
|
}
|
|
}
|