170 lines
5.8 KiB
Dart
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] تم قطع الاتصال (وضع التوصيل مطفأ)');
|
|
}
|
|
}
|