feat: refactor financial wallet UI components and add offline map service support

This commit is contained in:
Hamza-Ayed
2026-04-21 00:35:30 +03:00
parent 4293d20561
commit b92db3bb39
99 changed files with 22888 additions and 27387 deletions

View File

@@ -1,15 +1,17 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ui';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:flutter_background_service_android/flutter_background_service_android.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
import 'package:socket_io_client/socket_io_client.dart' as IO;
import 'package:flutter_overlay_window/flutter_overlay_window.dart' as Overlay;
import 'package:get_storage/get_storage.dart';
import 'package:geolocator/geolocator.dart' as geo;
import '../../constant/box_name.dart';
import '../firebase/local_notification.dart';
const String notificationChannelId = 'driver_service_channel';
const int notificationId = 888;
@@ -35,14 +37,18 @@ Future<bool> onStart(ServiceInstance service) async {
IO.OptionBuilder()
.setTransports(['websocket'])
.disableAutoConnect()
.setQuery({'driver_id': driverId, 'token': token})
.setQuery({
'driver_id': driverId,
'token': token,
'EIO': '3', // توافقية مع Workerman
})
.setReconnectionAttempts(double.infinity)
.build());
socket.connect();
socket.onConnect((_) {
print("✅ Background Service: Socket Connected!");
print("✅ Background Service: Socket Connected! ID: ${socket?.id}");
if (service is AndroidServiceInstance) {
flutterLocalNotificationsPlugin.show(
id: notificationId,
@@ -70,39 +76,94 @@ Future<bool> onStart(ServiceInstance service) async {
final box = GetStorage();
bool isAppInForeground = box.read(BoxName.isAppInForeground) ?? false;
// 🔥 Check إضافي: هل الـ Overlay مفتوح بالفعل؟
bool overlayActive = await Overlay.FlutterOverlayWindow.isActive();
// 🔥 Check إضافي: هل الـ Overlay مفتوح بالفعل؟ (للأندرويد فقط)
bool overlayActive = false;
if (Platform.isAndroid) {
overlayActive = await Overlay.FlutterOverlayWindow.isActive();
}
if (isAppInForeground || overlayActive) {
print("🛑 App is FOREGROUND or Overlay already shown. Skipping.");
return;
}
// عرض الـ Overlay
print("🚀 App is BACKGROUND. Showing Overlay...");
try {
await Overlay.FlutterOverlayWindow.showOverlay(
enableDrag: true,
overlayTitle: "طلب جديد",
overlayContent: "لديك طلب جديد وصل للتو!",
flag: OverlayFlag.focusPointer,
positionGravity: PositionGravity.auto,
height: WindowSize.matchParent,
width: WindowSize.matchParent,
startPosition: const OverlayPosition(0, -30),
);
await Overlay.FlutterOverlayWindow.shareData(data);
} catch (e) {
print("Overlay Error: $e");
// عرض الـ Overlay (للأندرويد فقط)
if (Platform.isAndroid) {
print("🚀 App is BACKGROUND. Showing Overlay...");
try {
await Overlay.FlutterOverlayWindow.showOverlay(
enableDrag: true,
overlayTitle: "طلب جديد",
overlayContent: "لديك طلب جديد وصل للتو!",
flag: OverlayFlag.focusPointer,
positionGravity: PositionGravity.auto,
height: WindowSize.matchParent,
width: WindowSize.matchParent,
startPosition: const OverlayPosition(0, -30),
);
await Overlay.FlutterOverlayWindow.shareData(data);
} catch (e) {
print("Overlay Error: $e");
}
} else if (Platform.isIOS) {
// على iOS، نظهر إشعاراً عادياً لأن الـ Overlay غير موجود
flutterLocalNotificationsPlugin.show(
id: 1002,
title: "طلب رحلة جديد 🚖",
body: "لديك طلب رحلة جديد، افتح التطبيق للموافقة عليه",
notificationDetails: const NotificationDetails(
iOS: DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
),
),
payload: jsonEncode(data));
}
});
}
service.on('stopService').listen((event) {
socket?.disconnect();
socket?.clearListeners();
socket?.dispose();
service.stopSelf();
});
// 🔥 Location management in background isolate (Using Geolocator)
geo.Position? latestPos;
// Listen to location changes continuously in the background
geo.Geolocator.getPositionStream(
locationSettings: geo.AndroidSettings(
accuracy: geo.LocationAccuracy.high,
distanceFilter: 10,
intervalDuration: const Duration(seconds: 10),
),
).listen((pos) {
latestPos = pos;
});
// 🔥 MERCY HEARTBEAT: Send location every 2 minutes to keep driver active in 'raids'
Timer.periodic(const Duration(minutes: 2), (timer) async {
if (socket != null && socket.connected && latestPos != null) {
try {
socket.emit('update_location', {
'driver_id': driverId,
'lat': latestPos!.latitude,
'lng': latestPos!.longitude,
'heading': latestPos!.heading,
'speed': latestPos!.speed * 3.6,
'status': box.read(BoxName.statusDriverLocation) ?? 'on',
'source': 'background_heartbeat'
});
print(
"💓 Background Mercy Heartbeat Sent: ${latestPos!.latitude}, ${latestPos!.longitude}");
} catch (e) {
print("❌ Background Heartbeat Error: $e");
}
}
});
Timer.periodic(const Duration(seconds: 30), (timer) async {
if (service is AndroidServiceInstance) {
if (await service.isForegroundService()) {

View File

@@ -1,3 +1,4 @@
/*
import 'dart:convert';
import 'dart:io';
@@ -234,3 +235,4 @@ class CameraClassController extends GetxController {
super.onClose();
}
}
*/

View File

@@ -604,6 +604,59 @@ class CRUD {
);
return json.decode(response.body);
}
Future<dynamic> getMapSaas({
required String link,
}) async {
var url = Uri.parse(link);
try {
var response = await http.get(
url,
headers: {
'Content-Type': 'application/json',
'x-api-key': Env.mapSaasKey,
},
);
Log.print('link -MapSaas: $link');
Log.print('response -MapSaas: ${response.body}');
if (response.statusCode == 200) {
return jsonDecode(response.body);
}
Log.print('MapSaas Error: ${response.statusCode} - ${response.body}');
return null;
} catch (e) {
Log.print('MapSaas Exception: $e');
return null;
}
}
Future<dynamic> postMapSaas({
required String link,
required Map<String, dynamic> payload,
}) async {
var url = Uri.parse(link);
try {
var response = await http.post(
url,
body: jsonEncode(payload),
headers: {
'Content-Type': 'application/json',
'x-api-key': Env.mapSaasKey,
},
);
Log.print('post -MapSaas link: $link');
Log.print('post -MapSaas payload: $payload');
Log.print('post -MapSaas response: ${response.body}');
if (response.statusCode == 200 || response.statusCode == 201) {
return jsonDecode(response.body);
}
Log.print('MapSaas Post Error: ${response.statusCode} - ${response.body}');
return null;
} catch (e) {
Log.print('MapSaas Post Exception: $e');
return null;
}
}
}
class NoInternetException implements Exception {

View File

@@ -1,7 +1,11 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:get/get_navigation/src/extension_navigation.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
import 'package:sefer_driver/views/widgets/mydialoug.dart';
import 'background_service.dart';
@@ -28,18 +32,19 @@ class PermissionsHelper {
if (Platform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo;
// Android 13+ (API 33+) يحتاج إذن POST_NOTIFICATIONS
if (androidInfo.version.sdkInt >= 33) {
final status = await Permission.notification.request();
if (status.isDenied) {
print('⚠️ إذن الإشعارات مرفوض');
mySnackbarWarning(
"يرجى منح صلاحية الإشعارات لضمان وصول الطلبات إليك");
return false;
}
if (status.isPermanentlyDenied) {
print('⚠️ إذن الإشعارات مرفوض بشكل دائم - افتح الإعدادات');
await openAppSettings();
mySnackbarWarning('يرجى فتح الإعدادات وتفعيل صلاحية الإشعارات');
return false;
}
}
@@ -50,25 +55,31 @@ class PermissionsHelper {
/// طلب جميع الإذونات المطلوبة
static Future<bool> requestAllPermissions() async {
// إذن الإشعارات أولاً
bool notificationGranted = await requestNotificationPermission();
if (!notificationGranted) return false;
// إذن الإشعارات (اختياري)
await requestNotificationPermission();
// إذن الموقع
final locationStatus = await Permission.location.request();
if (!locationStatus.isGranted) {
print('⚠️ إذن الموقع مرفوض');
// 1. طلب إذن الموقع الأساسي فقط إذا كان مرفوضاً
var status = await Permission.location.status;
if (status.isDenied) {
status = await Permission.location.request();
}
if (status.isPermanentlyDenied) {
_showSettingsDialog('الموقع');
return false;
}
// إذن الموقع في الخلفية
if (Platform.isAndroid) {
final bgLocationStatus = await Permission.locationAlways.request();
if (!bgLocationStatus.isGranted) {
print('⚠️ إذن الموقع في الخلفية مرفوض');
}
}
return status.isGranted || status.isLimited;
}
return true;
static void _showSettingsDialog(String permissionName) {
MyDialog().getDialog(
'صلاحية $permissionName مطلوبة',
'لقد قمت برفض صلاحية $permissionName سابقاً. يرجى تفعيلها من الإعدادات لتمكين التطبيق من العمل.',
() async {
await openAppSettings();
Get.back();
},
);
}
}

View File

@@ -1,15 +1,13 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:geolocator/geolocator.dart' as geo;
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'package:location/location.dart';
import 'package:battery_plus/battery_plus.dart';
import 'package:permission_handler/permission_handler.dart' as ph;
import 'package:sefer_driver/views/home/Captin/orderCaptin/order_request_page.dart';
import 'package:socket_io_client/socket_io_client.dart' as IO;
import 'package:sefer_driver/constant/table_names.dart';
import 'package:trip_overlay_plugin/trip_overlay_plugin.dart';
@@ -17,6 +15,7 @@ import '../../constant/box_name.dart';
import '../../constant/links.dart';
import '../../main.dart';
import '../../print.dart';
import '../firebase/local_notification.dart';
import '../home/captin/home_captain_controller.dart';
import '../home/captin/map_driver_controller.dart';
import '../home/payment/captain_wallet_controller.dart';
@@ -63,6 +62,7 @@ class LocationController extends GetxController with WidgetsBindingObserver {
bool _isPowerSavingMode = false;
final List<Map<String, dynamic>> _trackBuffer = [];
final List<Map<String, dynamic>> _behaviorBuffer = [];
LatLng? _lastPosForDistance;
LatLng? _lastRecordedRealLoc;
@@ -137,26 +137,22 @@ class LocationController extends GetxController with WidgetsBindingObserver {
Log.print("📱 Lifecycle: App is in FOREGROUND");
box.write(BoxName.isAppInForeground, true);
// إيقاف خدمة الخلفية لأننا في الواجهة الآن
// إيقاف خدمة الخلفية
BackgroundServiceHelper.stopService();
// التأكد من أن السوكيت متصل، وإذا لا، نعيد الاتصال فوراً
if (socket == null || !socket!.connected) {
Log.print("🔄 Socket disconnected in background. Reconnecting now...");
if (socket == null || (!socket!.connected && !_isInitializingSocket)) {
Log.print("🔄 Initializing Socket on resume...");
initSocket();
} else {
// إذا كان متصلاً، ننعش المستمعين فقط للتأكد
_setupSocketListeners();
}
} else if (state == AppLifecycleState.paused ||
state == AppLifecycleState.detached) {
Log.print("📱 Lifecycle: App is in BACKGROUND");
box.write(BoxName.isAppInForeground, false);
// تشغيل خدمة الخلفية لضمان بقاء التطبيق حياً
BackgroundServiceHelper.startService();
// ملاحظة: لا نقطع السوكيت هنا، نتركه يعمل قدر الإمكان
// تشغيل خدمة الخلفية للأندرويد لضمان بقاء التطبيق حياً
if (!Platform.isIOS) {
BackgroundServiceHelper.startService();
}
}
}
@@ -179,69 +175,107 @@ class LocationController extends GetxController with WidgetsBindingObserver {
// ====== Socket Logic (Improved) ======
// ===================================================================
bool _isInitializingSocket = false;
void initSocket() {
// منع الاستدعاءات المتداخلة التي تسبب قتل الاتصال قبل اكتماله
if (_isInitializingSocket) {
Log.print("⏳ Socket is already initializing. Skipping redundant call.");
return;
}
if (socket != null && socket!.connected) {
Log.print("✅ Socket is already connected. No need to re-init.");
return;
}
String driverId = box.read(BoxName.driverID).toString();
String token = box.read(BoxName.tokenDriver).toString();
String platform = Platform.isIOS ? 'ios' : 'android';
// 1. إذا كان السوكيت موجوداً، فقط تأكد من اتصاله
_isInitializingSocket = true;
// تنظيف السوكيت القديم فقط إذا كان موجوداً وغير متصل
if (socket != null) {
if (!socket!.connected) {
Log.print("🟡 Socket exists but disconnected. Reconnecting...");
socket!.connect();
}
_setupSocketListeners(); // تحديث المستمعين
return;
Log.print("🧹 Cleaning up old socket instance...");
socket!.clearListeners();
socket!.dispose();
socket = null;
}
Log.print(
"🟡 [LocationController] Creating NEW Socket for Driver: $driverId");
"🟡 [LocationController] Initializing NEW Socket for Driver: $driverId");
// 2. إنشاء الاتصال
socket = IO.io(
'https://location.intaleq.xyz',
IO.OptionBuilder()
.setTransports(['websocket'])
.enableAutoConnect() // تفعيل إعادة الاتصال التلقائي
.setQuery(
{'driver_id': driverId, 'token': token, 'platform': platform})
.setReconnectionAttempts(double.infinity)
.setReconnectionDelay(2000)
.build());
try {
// العودة للـ Websocket حصراً لأنه الوحيد الذي ينجح في فتح القناة
socket = IO.io(
'https://location.intaleq.xyz',
IO.OptionBuilder()
.setTransports(['websocket'])
.setQuery({'driver_id': driverId, 'token': token, 'EIO': '3'})
.enableForceNew()
.build());
socket!.connect();
_setupSocketListeners();
_setupSocketListeners();
socket!.connect();
} catch (e) {
_isInitializingSocket = false;
Log.print("❌ Socket Initialization Exception: $e");
}
}
// دالة منفصلة لضمان عدم تكرار المستمعين
void _setupSocketListeners() {
if (socket == null) return;
// تنظيف القديم أولاً
socket!.off('connect');
socket!.off('disconnect');
socket!.off('new_ride_request');
socket!.off('ride_cancelled');
socket!.off('connect_error');
socket!.off('error');
socket!.onConnect((_) {
Log.print('✅ Socket Connected! ID: ${socket?.id}');
isSocketConnected = true;
_startHeartbeat();
_isInitializingSocket = false;
// ننتظر قليلاً للتأكد من تعبئة الـ IDs
Future.delayed(const Duration(milliseconds: 1000), () {
String? sid = socket?.id;
String? eid = socket?.io.engine?.id;
Log.print(
'✅ Socket Connected! ID: ${sid ?? eid ?? 'N/A'} (SID: $sid, EID: $eid)');
if (sid != null || eid != null) {
isSocketConnected = true;
_startHeartbeat();
}
});
});
socket!.onDisconnect((_) {
Log.print('❌ Socket Disconnected');
socket!.onDisconnect((data) {
_isInitializingSocket = false;
Log.print('❌ Socket Disconnected: $data');
isSocketConnected = false;
_stopHeartbeat();
});
socket!.onConnectError((err) {
_isInitializingSocket = false;
Log.print('❌ Socket Connect Error: $err');
});
socket!.onConnectTimeout((data) {
_isInitializingSocket = false;
Log.print('❌ Socket Connect Timeout: $data');
});
socket!.onError((err) {
_isInitializingSocket = false;
Log.print('❌ Socket General Error: $err');
});
socket!.on('reconnect_attempt', (attempt) {
Log.print('🔄 Socket Reconnecting... Attempt: $attempt');
});
// 🔥 الاستماع للطلبات الجديدة
socket!.on('new_ride_request', (data) {
Log.print("🔔 Socket: New Ride Request Arrived!");
@@ -296,8 +330,17 @@ class LocationController extends GetxController with WidgetsBindingObserver {
if (!isAppInForeground) {
Log.print(
"🛑 التطبيق في الخلفية. السوكيت سيتجاهل التوجيه ويترك المهمة للـ Overlay.");
return; // 👈 هذا السطر يمنع السوكيت من إكمال العمل وفتح الصفحة
"📱 [LocationController] Order received in background (iOS/Android). Source: $source");
if (Platform.isIOS) {
// على iOS، نقوم بإظهار إشعار محلي لأن الـ Overlay غير مدعوم
NotificationController().showNotification(
"طلب رحلة جديد 🚖",
"لديك طلب رحلة جديد، افتح التطبيق للموافقة عليه",
jsonEncode(rideData),
'ding.wav');
}
return;
}
try {
@@ -309,14 +352,23 @@ class LocationController extends GetxController with WidgetsBindingObserver {
// 3. تجهيز البيانات (DriverList)
List<dynamic> driverList = [];
if (rideData.length > 0) {
var sortedKeys = rideData.keys.map((e) => int.tryParse(e) ?? 0).toList()
..sort();
if (rideData.isNotEmpty) {
var sortedKeys = rideData.keys
.where((e) => int.tryParse(e) != null)
.map((e) => int.parse(e))
.toList()..sort();
for (var key in sortedKeys) {
driverList.add(rideData[key.toString()]);
}
}
// الحماية ضد البنية غير المكتملة
if (driverList.length <= 16) {
Log.print("❌ Socket Error: Parsed driver list is incomplete.");
return;
}
// 4. إغلاق النافذة (إن وجدت بالخطأ) والتنقل
try {
if (await TripOverlayPlugin.isOverlayActive()) {
@@ -459,21 +511,16 @@ class LocationController extends GetxController with WidgetsBindingObserver {
_uploadBatchTimer?.cancel();
_socketHeartbeat?.cancel();
if (socket != null && socket!.connected) {
String driverId = box.read(BoxName.driverID).toString();
socket!.emit('update_location', {
'driver_id': driverId,
'lat': myLocation.latitude,
'lng': myLocation.longitude,
'heading': heading,
'speed': speed * 3.6,
'status': 'close', // Changed to off
'distance': totalDistance
});
socket!.disconnect();
if (socket != null) {
socket!.clearListeners();
socket!
.dispose(); // استخدام dispose بدلاً من disconnect لضمان تحرير الموارد على iOS
}
if (!Platform.isIOS) {
await BackgroundServiceHelper.stopService();
}
await BackgroundServiceHelper.stopService();
socket = null;
isSocketConnected = false;
_isReady = false;
@@ -526,16 +573,31 @@ class LocationController extends GetxController with WidgetsBindingObserver {
Future<void> _flushBufferToServer() async {
if (_trackBuffer.isEmpty) return;
List<Map<String, dynamic>> batch = List.from(_trackBuffer);
_trackBuffer.clear();
int itemsToTake = _trackBuffer.length > 100 ? 100 : _trackBuffer.length;
List<Map<String, dynamic>> batch = _trackBuffer.sublist(0, itemsToTake);
final String driverId = (box.read(BoxName.driverID) ?? '').toString();
try {
await CRUD().post(
var res = await CRUD().post(
link: '${AppLink.locationServer}/add_batch.php',
payload: {'driver_id': driverId, 'batch_data': jsonEncode(batch)},
);
if (res != 'failure') {
_trackBuffer.removeRange(0, itemsToTake);
} else {
_enforceBufferLimit();
}
} catch (e) {
Log.print('❌ Failed to upload batch: $e');
_enforceBufferLimit();
}
}
void _enforceBufferLimit() {
if (_trackBuffer.length > 500) {
_trackBuffer.removeRange(0, _trackBuffer.length - 500);
Log.print("⚠️ Buffer limit enforced. Removed oldest entries.");
}
}
@@ -547,32 +609,65 @@ class LocationController extends GetxController with WidgetsBindingObserver {
if (level >= powerSaveExitLevel) _isPowerSavingMode = false;
if (previousMode != _isPowerSavingMode) {
_startBatchTimers();
startLocationUpdates();
_updateLocationSettings();
}
});
}
Future<void> _updateLocationSettings() async {
if (_locSub == null) return;
int interval = _isPowerSavingMode ? 10000 : 5000;
try {
await location.changeSettings(
accuracy: LocationAccuracy.navigation,
interval: interval,
distanceFilter: _isPowerSavingMode ? 20 : 10,
);
Log.print("🔋 Location settings updated. Power Save: $_isPowerSavingMode");
} catch (e) {
Log.print("❌ Failed to update location settings: $e");
}
}
Future<void> _saveBehaviorIfMoved(LatLng pos, DateTime now,
{required double currentSpeed}) async {
final dist =
(_lastSqlLoc == null) ? 999.0 : _calculateDistance(_lastSqlLoc!, pos);
if (dist < 15.0) return;
final accel = _calcAcceleration(currentSpeed, now) ?? 0.0;
try {
await sql.insertData({
'driver_id': (box.read(BoxName.driverID) ?? '').toString(),
'latitude': pos.latitude,
'longitude': pos.longitude,
'acceleration': accel,
'created_at': now.toIso8601String(),
'updated_at': now.toIso8601String(),
}, TableName.behavior);
_lastSqlLoc = pos;
} catch (e) {
Log.print('SQLite Error: $e');
_lastSqlLoc = pos;
_behaviorBuffer.add({
'driver_id': (box.read(BoxName.driverID) ?? '').toString(),
'latitude': pos.latitude,
'longitude': pos.longitude,
'acceleration': accel,
'created_at': now.toIso8601String(),
'updated_at': now.toIso8601String(),
});
if (_behaviorBuffer.length >= 10) {
_flushBehaviorBuffer();
}
}
void _flushBehaviorBuffer() {
if (_behaviorBuffer.isEmpty) return;
List<Map<String, dynamic>> batch = List.from(_behaviorBuffer);
_behaviorBuffer.clear();
Future.microtask(() async {
try {
for (var data in batch) {
await sql.insertData(data, TableName.behavior);
}
} catch (e) {
Log.print('SQLite Batch Insert Error: $e');
}
});
}
// استبدال دالة Haversine اليدوية بـ Geolocator في باقي الكود أيضاً
// لأنها تعتمد على C++ في الأندرويد و Obj-C في الآيفون (Native Speed)
double _calculateDistance(LatLng a, LatLng b) {

View File

@@ -217,8 +217,12 @@ class SecurityHelper {
isNotTrust = await JailbreakRootDetection.instance.isNotTrust;
isJailBroken = await JailbreakRootDetection.instance.isJailBroken;
isRealDevice = await JailbreakRootDetection.instance.isRealDevice;
isOnExternalStorage =
await JailbreakRootDetection.instance.isOnExternalStorage;
// This method is only relevant/implemented for Android
if (Platform.isAndroid) {
isOnExternalStorage =
await JailbreakRootDetection.instance.isOnExternalStorage;
}
List<JailbreakIssue> issues =
await JailbreakRootDetection.instance.checkForIssues;
@@ -230,7 +234,6 @@ class SecurityHelper {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
bundleId = packageInfo.packageName;
if (bundleId.isNotEmpty) {
// Pass the CORRECT bundle ID to isTampered
isTampered = await JailbreakRootDetection.instance.isTampered(bundleId);
}
} catch (e) {