Food delivery driver module + masked calls + TURN
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
// 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] تم قطع الاتصال (وضع التوصيل مطفأ)');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user