Update codebase
This commit is contained in:
@@ -11,6 +11,7 @@ import 'package:flutter_overlay_window/flutter_overlay_window.dart' as Overlay;
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/links.dart';
|
||||
import 'package:trip_overlay_plugin/trip_overlay_plugin.dart';
|
||||
import '../firebase/local_notification.dart';
|
||||
|
||||
const String notificationChannelId = 'driver_service_channel';
|
||||
@@ -136,11 +137,172 @@ Future<bool> onStart(ServiceInstance service) async {
|
||||
payload: jsonEncode(data));
|
||||
}
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// 🚫 الرحلة أخذها سائق آخر
|
||||
//
|
||||
// نافذة الأوفرلي تعيش في عزلة منفصلة بلا سوكِت خاص بها، فلا تعلم
|
||||
// بشيء من نفسها. وهذه العزلة الخلفية هي الوحيدة التي تسمع الحدث
|
||||
// والتطبيق مغلق. بدون هذا المستمع تبقى النافذة معروضة وعدّادها
|
||||
// يعمل على رحلة لم تعد موجودة، ويضغط السائق "قبول" فيجدها مأخوذة.
|
||||
//
|
||||
// نغلق النافذتين معاً: نافذة فلاتر (مسار السوكِت) والنافذة الأصلية
|
||||
// (مسار FCM) — لا نعرف أيّهما معروضة الآن.
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
socket.on('ride_taken', (data) async {
|
||||
try {
|
||||
await GetStorage.init();
|
||||
final box = GetStorage();
|
||||
final String myDriverId = box.read(BoxName.driverID).toString();
|
||||
final String whoTookIt = data?['taken_by_driver_id']?.toString() ?? '';
|
||||
|
||||
// لا نغلق على من فاز بها — تطبيقه في طريقه لفتح شاشة الرحلة.
|
||||
if (whoTookIt == myDriverId) return;
|
||||
|
||||
bool closedAny = false;
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
if (await Overlay.FlutterOverlayWindow.isActive()) {
|
||||
await Overlay.FlutterOverlayWindow.closeOverlay();
|
||||
closedAny = true;
|
||||
}
|
||||
if (await TripOverlayPlugin.isOverlayActive()) {
|
||||
await TripOverlayPlugin.hideOverlay();
|
||||
closedAny = true;
|
||||
}
|
||||
}
|
||||
|
||||
// الإشعار فقط إن كان هناك ما يُغلق فعلاً — وإلا أزعجنا سائقاً
|
||||
// لم يُعرض عليه شيء أصلاً (الحدث يُبثّ للجميع).
|
||||
if (closedAny) {
|
||||
flutterLocalNotificationsPlugin.show(
|
||||
id: 1003,
|
||||
title: "الطلب لم يعد متاحاً",
|
||||
body: "قام سائق آخر بقبول هذه الرحلة",
|
||||
notificationDetails: const NotificationDetails(
|
||||
iOS: DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: false,
|
||||
presentSound: false,
|
||||
),
|
||||
),
|
||||
payload: '');
|
||||
}
|
||||
} catch (e) {
|
||||
print("⚠️ ride_taken handler error: $e");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// سوكيت الطعام في العزلة الخلفية — مستقل تماماً عن سوكيت الرحلات أعلاه
|
||||
// (خادم وبورت وغرفة مختلفة). بدونه لا يصل عرض توصيل والتطبيق مغلق، لأن
|
||||
// FoodSocketService يعيش في العزلة الرئيسية التي يوقفها أندرويد.
|
||||
// نفتحه فقط إن كان وضع التوصيل مفعّلاً — لا اتصال دائم بلا داعٍ.
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
IO.Socket? foodSocket;
|
||||
|
||||
void connectFoodSocket() {
|
||||
if (foodSocket != null || driverId.isEmpty) return;
|
||||
|
||||
foodSocket = IO.io(
|
||||
AppLink.foodSocketUrl,
|
||||
IO.OptionBuilder()
|
||||
.setTransports(['websocket'])
|
||||
.setQuery({
|
||||
'role': 'driver',
|
||||
'id': driverId,
|
||||
'jwt': token,
|
||||
'platform': Platform.isIOS ? 'ios' : 'android',
|
||||
})
|
||||
.enableReconnection()
|
||||
.setReconnectionDelay(3000)
|
||||
.build());
|
||||
|
||||
foodSocket!.onConnect((_) => print('🍔 Background: food socket connected'));
|
||||
|
||||
foodSocket!.on('food_delivery_offer', (data) async {
|
||||
print('🍔 Background: food delivery offer received');
|
||||
await GetStorage.init();
|
||||
final box = GetStorage();
|
||||
|
||||
// الواجهة أمام السائق تتكفّل بالعرض إن كان التطبيق مفتوحاً
|
||||
final bool isAppInForeground = box.read(BoxName.isAppInForeground) ?? false;
|
||||
if (isAppInForeground) return;
|
||||
|
||||
Map<String, dynamic>? payload;
|
||||
if (data is Map) {
|
||||
payload = Map<String, dynamic>.from(data);
|
||||
} else if (data is List && data.isNotEmpty && data.first is Map) {
|
||||
payload = Map<String, dynamic>.from(data.first as Map);
|
||||
}
|
||||
if (payload == null) return;
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
final shown = await TripOverlayPlugin.showFoodOverlay(
|
||||
FoodOrderOverlayData(
|
||||
orderId: payload['order_id'].toString(),
|
||||
merchantName: payload['merchant_name_ar']?.toString() ?? 'مطعم',
|
||||
merchantAddress: payload['merchant_address']?.toString() ?? '',
|
||||
deliveryFeeText: _formatFoodPrice(payload['delivery_fee']),
|
||||
cashToCollectText: _formatFoodPrice(payload['cash_to_collect']),
|
||||
distanceText: '',
|
||||
itemsCount: int.tryParse(payload['items_count']?.toString() ?? '') ?? 0,
|
||||
isCash: payload['payment_method']?.toString() == 'cash',
|
||||
),
|
||||
autoCloseSeconds: 20,
|
||||
);
|
||||
if (shown) return;
|
||||
}
|
||||
|
||||
// iOS أو تعذّرت النافذة — إشعار على قناة التوصيل الخاصة
|
||||
flutterLocalNotificationsPlugin.show(
|
||||
id: 7001,
|
||||
title: '🍔 عرض توصيل جديد',
|
||||
body: '${payload['merchant_name_ar'] ?? 'مطعم'} — '
|
||||
'${_formatFoodPrice(payload['delivery_fee'])}',
|
||||
notificationDetails: const NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
'food_delivery_offer_channel',
|
||||
'عروض التوصيل',
|
||||
importance: Importance.max,
|
||||
priority: Priority.max,
|
||||
fullScreenIntent: true,
|
||||
category: AndroidNotificationCategory.call,
|
||||
),
|
||||
iOS: DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
),
|
||||
),
|
||||
payload: jsonEncode({'food_event': 'offer', 'order_id': payload['order_id']}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (box.read(BoxName.foodDeliveryModeEnabled) == true) {
|
||||
connectFoodSocket();
|
||||
}
|
||||
|
||||
// تفعيل/إطفاء وضع التوصيل من الواجهة ينعكس هنا بلا إعادة تشغيل الخدمة
|
||||
service.on('foodModeChanged').listen((event) {
|
||||
final enabled = event?['enabled'] == true;
|
||||
if (enabled) {
|
||||
connectFoodSocket();
|
||||
} else {
|
||||
foodSocket?.clearListeners();
|
||||
foodSocket?.dispose();
|
||||
foodSocket = null;
|
||||
}
|
||||
});
|
||||
|
||||
service.on('stopService').listen((event) {
|
||||
socket?.clearListeners();
|
||||
socket?.dispose();
|
||||
foodSocket?.clearListeners();
|
||||
foodSocket?.dispose();
|
||||
service.stopSelf();
|
||||
});
|
||||
|
||||
@@ -185,6 +347,14 @@ Future<bool> onStart(ServiceInstance service) async {
|
||||
return true;
|
||||
}
|
||||
|
||||
// نفس قاعدة foodFormatPrice في نماذج الطعام — مكرّرة هنا عمداً لأن العزلة
|
||||
// الخلفية يجب أن تبقى خفيفة الاعتماديات ولا تسحب طبقة النماذج كاملة.
|
||||
String _formatFoodPrice(dynamic smallestUnit) {
|
||||
final v = int.tryParse(smallestUnit?.toString() ?? '') ?? 0;
|
||||
if (v == 0) return '';
|
||||
return '${(v / 1000).toStringAsFixed(3)} د.أ';
|
||||
}
|
||||
|
||||
class BackgroundServiceHelper {
|
||||
static Future<void> initialize() async {
|
||||
final service = FlutterBackgroundService();
|
||||
|
||||
Reference in New Issue
Block a user