// food_socket_service.dart — قناة سوكيت مستقلة تماماً لوحدة التوصيل // // لماذا سوكيت منفصل ولا نركب على سوكيت الرحلات (LocationController.socket)؟ // • خادمان منفصلان فعلاً: food_socket.php (4040) مقابل driver_socket (2020)، // وغرفة السائق هناك courier_food_{id} بمصادقة JWT مختلفة الاستعلام. // • عزل الأعطال: سقوط سوكيت الطعام لا يقطع توزيع الرحلات، وهو المسار الحرج. // • دورة حياة مختلفة: نتصل فقط أثناء «وضع التوصيل» وننفصل عند إطفائه، // بينما سوكيت الرحلات يبقى حياً طوال مناوبة السائق. import 'dart:async'; import 'dart:io'; import 'package:socket_io_client/socket_io_client.dart' as IO; import '../../constant/box_name.dart'; import '../../constant/links.dart'; import '../../main.dart'; import '../../print.dart'; typedef FoodOfferHandler = void Function(Map payload); typedef FoodOrderUpdateHandler = void Function(Map payload); class FoodSocketService { FoodSocketService._(); static final FoodSocketService instance = FoodSocketService._(); IO.Socket? _socket; bool _isConnecting = false; Timer? _heartbeat; FoodOfferHandler? onOffer; FoodOrderUpdateHandler? onOrderUpdate; FoodOrderUpdateHandler? onIncomingCall; void Function(bool connected)? onConnectionChanged; bool get isConnected => _socket?.connected == true; Future connect() async { if (isConnected || _isConnecting) return; final driverId = box.read(BoxName.driverID)?.toString() ?? ''; final token = box.read(BoxName.tokenDriver)?.toString() ?? ''; if (driverId.isEmpty || token.isEmpty) { Log.print('🍔 [FoodSocket] لا يوجد driverID/token — تخطّي الاتصال'); return; } _isConnecting = true; _disposeSocket(); try { _socket = IO.io( AppLink.foodSocketUrl, IO.OptionBuilder() .setTransports(['websocket']) // food_socket.php يتحقق: role=driver ويطابق user_id داخل الـ JWT مع id .setQuery({ 'role': 'driver', 'id': driverId, 'jwt': token, 'platform': Platform.isIOS ? 'ios' : 'android', }) .enableForceNew() .enableReconnection() .setReconnectionDelay(2000) .build(), ); _bindListeners(); _socket!.connect(); } catch (e) { _isConnecting = false; Log.print('❌ [FoodSocket] فشل التهيئة: $e'); } } void _bindListeners() { final s = _socket; if (s == null) return; s.onConnect((_) { _isConnecting = false; Log.print('✅ [FoodSocket] متصل — غرفة courier_food'); onConnectionChanged?.call(true); _startHeartbeat(); }); s.onDisconnect((_) { Log.print('⚠️ [FoodSocket] انقطع الاتصال'); onConnectionChanged?.call(false); _stopHeartbeat(); }); s.onConnectError((e) { _isConnecting = false; Log.print('❌ [FoodSocket] خطأ اتصال: $e'); onConnectionChanged?.call(false); }); s.onError((e) => Log.print('❌ [FoodSocket] خطأ: $e')); // عرض توصيل جديد — الحمولة كاملة من foodBuildCourierOfferPayload، // فشاشة العرض تُبنى فوراً بلا نداء HTTP إضافي (المهلة 20 ثانية فقط). s.on('food_delivery_offer', (data) { final map = _asMap(data); if (map == null) return; Log.print('🍔 [FoodSocket] عرض توصيل #${map['order_id']}'); onOffer?.call(map); }); // مكالمة واردة من الزبون عبر القناة المقنّعة — لا رقم هاتف في الحمولة، // فقط session_id ينضم به الطرفان لخادم الإشارات. s.on('food_incoming_call', (data) { final map = _asMap(data); if (map == null) return; Log.print('🍔 [FoodSocket] مكالمة واردة على الطلب #${map['order_id']}'); onIncomingCall?.call(map); }); // تحديث حالة طلب قائم (المطعم ألغى، النظام ألغى، إلخ) s.on('food_order_update', (data) { final map = _asMap(data); if (map == null) return; Log.print('🍔 [FoodSocket] تحديث طلب #${map['order_id']} → ${map['status']}'); onOrderUpdate?.call(map); }); } // السوكيت ناقل إشعار لا مخزن حالة — أي حمولة غير مفهومة تُهمَل بصمت // ويصحّح التطبيق نفسه من courier/active.php. Map? _asMap(dynamic data) { try { if (data is Map) return Map.from(data); if (data is List && data.isNotEmpty && data.first is Map) { return Map.from(data.first as Map); } } catch (e) { Log.print('❌ [FoodSocket] حمولة غير صالحة: $e'); } return null; } void _startHeartbeat() { _stopHeartbeat(); _heartbeat = Timer.periodic(const Duration(seconds: 25), (_) { if (isConnected) _socket!.emit('heartbeat', {}); }); } void _stopHeartbeat() { _heartbeat?.cancel(); _heartbeat = null; } void _disposeSocket() { if (_socket == null) return; _socket!.clearListeners(); _socket!.dispose(); _socket = null; } void disconnect() { _stopHeartbeat(); _isConnecting = false; _socket?.disconnect(); _disposeSocket(); onConnectionChanged?.call(false); Log.print('🍔 [FoodSocket] تم قطع الاتصال (وضع التوصيل مطفأ)'); } }