Files
tripz-llc/apps/driver/lib/controller/food_delivery/food_socket_service.dart
T
2026-08-09 16:56:13 +03:00

170 lines
5.8 KiB
Dart

// 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<String, dynamic> payload);
typedef FoodOrderUpdateHandler = void Function(Map<String, dynamic> 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<void> 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<String, dynamic>? _asMap(dynamic data) {
try {
if (data is Map) return Map<String, dynamic>.from(data);
if (data is List && data.isNotEmpty && data.first is Map) {
return Map<String, dynamic>.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] تم قطع الاتصال (وضع التوصيل مطفأ)');
}
}