Fixes & Updates - 2026-06-01: Integrate Back-End v3 updates, fix call/connection issues across apps

This commit is contained in:
Hamza-Ayed
2026-06-01 23:35:29 +03:00
parent 8f555691b9
commit cbf693c804
56 changed files with 6091 additions and 1217 deletions

View File

@@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import 'dart:io';
import 'dart:convert';
import '../../constant/info.dart';
import '../../constant/links.dart';
import '../../constant/colors.dart';
import '../../print.dart';
import 'crud.dart';
class AppUpdateController extends GetxController {
@override
void onInit() {
super.onInit();
// الفحص التلقائي عند التشغيل لتحديثات المتجر
checkSmartUpdate();
}
// ======================================================================
// الدالة الذكية المدمجة (الآن تفحص المتجر فقط لأن Shorebird يعمل تلقائياً بالخلفية)
// ======================================================================
Future<void> checkSmartUpdate() async {
Log.print("🔄 بدء فحص تحديثات المتجر...");
// 1. فحص تحديث المتجر (Native Update)
await _checkStoreUpdate();
}
// ======================================================================
// 1. تحديث المتجر الأساسي
// ======================================================================
Future<bool> _checkStoreUpdate() async {
try {
final packageInfo = await PackageInfo.fromPlatform();
final currentBuildNumber = packageInfo.buildNumber;
// استخدام نفس الـ Endpoint والمعايير الموجودة في التطبيق
var response = await CRUD().get(link: AppLink.packageInfo, payload: {
"platform": Platform.isAndroid ? 'android' : 'ios',
"appName": AppInformation.appVersion,
});
if (response != 'failure') {
var decoded = jsonDecode(response);
if (decoded['status'] == 'success' && decoded['message'] != null && decoded['message'].isNotEmpty) {
String latestBuildNumber = decoded['message'][0]['version'].toString();
// مقارنة الـ Build Number
if (latestBuildNumber != currentBuildNumber) {
_showStoreUpdateDialog();
return true;
}
}
}
} catch (e) {
Log.print("❌ Store update check error: $e");
}
return false;
}
// ======================================================================
// دوال مساعدة
// ======================================================================
void _showStoreUpdateDialog() {
final String storeUrl = Platform.isAndroid
? 'https://play.google.com/store/apps/details?id=com.intaleq_driver'
: 'https://apps.apple.com/jo/app/intaleq-driver/id6482995159';
Get.defaultDialog(
title: "تحديث جديد متوفر".tr,
middleText: "يوجد إصدار جديد من التطبيق في المتجر، يرجى التحديث للحصول على الميزات الجديدة.".tr,
barrierDismissible: false,
onWillPop: () async => false,
confirm: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.primaryColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))
),
onPressed: () async {
if (await canLaunchUrl(Uri.parse(storeUrl))) {
await launchUrl(Uri.parse(storeUrl), mode: LaunchMode.externalApplication);
}
},
child: Text("تحديث الآن".tr, style: const TextStyle(color: Colors.white)),
),
);
}
}

View File

@@ -0,0 +1,80 @@
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:get/get.dart';
import 'package:just_audio/just_audio.dart';
import 'package:record/record.dart';
class AudioRecorderController extends GetxController {
AudioPlayer audioPlayer = AudioPlayer();
AudioRecorder recorder = AudioRecorder();
bool isRecording = false;
bool isPlaying = false;
bool isPaused = false;
String filePath = '';
String? selectedFilePath;
double currentPosition = 0;
double totalDuration = 0;
// Start recording
Future<void> startRecording({String? rideId}) async {
final bool isPermissionGranted = await recorder.hasPermission();
if (!isPermissionGranted) {
return;
}
final directory = await getApplicationDocumentsDirectory();
final String dateStr =
'${DateTime.now().year}-${DateTime.now().month.toString().padLeft(2, '0')}-${DateTime.now().day.toString().padLeft(2, '0')}';
// Generate a unique file name
String fileName = (rideId != null && rideId.isNotEmpty && rideId != 'yet' && rideId != 'null')
? '${dateStr}_$rideId.m4a'
: '$dateStr.m4a';
filePath = '${directory.path}/$fileName';
const config = RecordConfig(
encoder: AudioEncoder.aacLc,
sampleRate: 44100,
bitRate: 128000,
);
await recorder.start(config, path: filePath);
isRecording = true;
update();
}
// Stop recording
Future<void> stopRecording() async {
await recorder.stop();
isRecording = false;
isPaused = false;
update();
}
// Get a list of recorded files
Future<List<String>> getRecordedFiles() async {
final directory = await getApplicationDocumentsDirectory();
final files = await directory.list().toList();
return files
.map((file) => file.path)
.where((path) => path.endsWith('.m4a'))
.toList();
}
// Delete a specific recorded file
Future<void> deleteRecordedFile(String filePath) async {
final file = File(filePath);
if (await file.exists()) {
await file.delete();
update();
}
}
@override
void onClose() {
audioPlayer.dispose();
recorder.dispose();
super.onClose();
}
}

View File

@@ -1,5 +1,7 @@
import 'package:url_launcher/url_launcher.dart';
import 'dart:io';
import 'package:get/get.dart';
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
void showInBrowser(String url) async {
if (await canLaunchUrl(Uri.parse(url))) {
@@ -7,34 +9,42 @@ void showInBrowser(String url) async {
} else {}
}
Future<void> makePhoneCall(String phoneNumber) async {
String cleanAndFormatPhoneNumber(String phoneNumber) {
// 1. Clean the number
String formattedNumber = phoneNumber.replaceAll(RegExp(r'\s+'), '');
// 2. Format logic (Syria/International)
// 2. Format logic (Syria/Egypt/International)
if (formattedNumber.length > 6) {
if (formattedNumber.startsWith('09')) {
formattedNumber = '+963${formattedNumber.substring(1)}';
} else if (formattedNumber.startsWith('01') && formattedNumber.length == 11) {
formattedNumber = '+20${formattedNumber.substring(1)}';
} else if (formattedNumber.startsWith('00')) {
formattedNumber = '+${formattedNumber.substring(2)}';
} else if (!formattedNumber.startsWith('+')) {
formattedNumber = '+$formattedNumber';
}
}
return formattedNumber;
}
// 3. Create URI
final Uri launchUri = Uri(
scheme: 'tel',
path: formattedNumber,
);
Future<void> makePhoneCall(String phoneNumber) async {
String formattedNumber = cleanAndFormatPhoneNumber(phoneNumber);
if (!formattedNumber.startsWith('+963')) {
mySnackeBarError("Calling non-Syrian numbers is not supported".tr);
return;
}
// Create URI directly from String to avoid double encoding '+' as '%2B'
final Uri launchUri = Uri.parse('tel:$formattedNumber');
// 4. Execute with externalApplication mode
try {
// Attempt to launch directly without checking canLaunchUrl first
// (Sometimes canLaunchUrl returns false on some devices even if it works)
if (!await launchUrl(launchUri, mode: LaunchMode.externalApplication)) {
throw 'Could not launch $launchUri';
}
} catch (e) {
// Fallback: Try checking canLaunchUrl if the direct launch fails
if (await canLaunchUrl(launchUri)) {
await launchUrl(launchUri);
} else {
@@ -45,23 +55,30 @@ Future<void> makePhoneCall(String phoneNumber) async {
void launchCommunication(
String method, String contactInfo, String message) async {
String formattedContact = cleanAndFormatPhoneNumber(contactInfo);
// WhatsApp prefers the phone number without the '+' prefix
String whatsappContact = formattedContact.replaceAll('+', '');
String url;
if (Platform.isIOS) {
switch (method) {
case 'phone':
url = 'tel:$contactInfo';
if (!formattedContact.startsWith('+963')) {
mySnackeBarError("Calling non-Syrian numbers is not supported".tr);
return;
}
url = 'tel:$formattedContact';
break;
case 'sms':
url = 'sms:$contactInfo?body=${Uri.encodeComponent(message)}';
url = 'sms:$formattedContact?body=${Uri.encodeComponent(message)}';
break;
case 'whatsapp':
url =
'https://api.whatsapp.com/send?phone=$contactInfo&text=${Uri.encodeComponent(message)}';
'https://api.whatsapp.com/send?phone=$whatsappContact&text=${Uri.encodeComponent(message)}';
break;
case 'email':
url =
'mailto:$contactInfo?subject=Subject&body=${Uri.encodeComponent(message)}';
'mailto:$formattedContact?subject=Subject&body=${Uri.encodeComponent(message)}';
break;
default:
return;
@@ -69,27 +86,29 @@ void launchCommunication(
} else if (Platform.isAndroid) {
switch (method) {
case 'phone':
url = 'tel:$contactInfo';
if (!formattedContact.startsWith('+963')) {
mySnackeBarError("Calling non-Syrian numbers is not supported".tr);
return;
}
url = 'tel:$formattedContact';
break;
case 'sms':
url = 'sms:$contactInfo?body=${Uri.encodeComponent(message)}';
url = 'sms:$formattedContact?body=${Uri.encodeComponent(message)}';
break;
case 'whatsapp':
// Check if WhatsApp is installed
final bool whatsappInstalled =
await canLaunchUrl(Uri.parse('whatsapp://'));
if (whatsappInstalled) {
url =
'whatsapp://send?phone=$contactInfo&text=${Uri.encodeComponent(message)}';
'whatsapp://send?phone=$whatsappContact&text=${Uri.encodeComponent(message)}';
} else {
// Provide an alternative action, such as opening the WhatsApp Web API
url =
'https://api.whatsapp.com/send?phone=$contactInfo&text=${Uri.encodeComponent(message)}';
'https://api.whatsapp.com/send?phone=$whatsappContact&text=${Uri.encodeComponent(message)}';
}
break;
case 'email':
url =
'mailto:$contactInfo?subject=Subject&body=${Uri.encodeComponent(message)}';
'mailto:$formattedContact?subject=Subject&body=${Uri.encodeComponent(message)}';
break;
default:
return;

View File

@@ -54,8 +54,11 @@ class LocationController extends GetxController with WidgetsBindingObserver {
late final HomeCaptainController _homeCtrl;
late final CaptainWalletController _walletCtrl;
LatLng myLocation = const LatLng(0, 0);
double heading = 0.0;
LatLng myLocation = LatLng(
box.read('last_lat') ?? 0.0,
box.read('last_lng') ?? 0.0,
);
double heading = box.read('last_heading') ?? 0.0;
double speed = 0.0;
double totalDistance = 0.0;
bool _isReady = false;
@@ -379,7 +382,23 @@ class LocationController extends GetxController with WidgetsBindingObserver {
Log.print("Overlay check error: $e");
}
if (Get.currentRoute != '/OrderRequestPage') {
// 🔥 [Fix Active-Ride Guard] منع فتح صفحة الطلبات أثناء وجود السائق في رحلة نشطة
// هذا يمنع socket event جديد من تعطيل رحلة جارية
String? currentRideStatus = box.read(BoxName.rideStatus);
bool hasActiveRide = (currentRideStatus == 'Begin' ||
currentRideStatus == 'Apply' ||
currentRideStatus == 'Arrived');
String currentRoute = Get.currentRoute;
bool isOnMapPage = currentRoute.contains('MapPage') ||
currentRoute.contains('PassengerLocation');
if (hasActiveRide || isOnMapPage) {
Log.print(
"⛔ [LocationController] Ignoring new ride request — driver has active ride ($currentRideStatus) or is on map page ($currentRoute).");
return;
}
if (currentRoute != '/OrderRequestPage') {
Log.print("🚀 Socket: Navigating to OrderRequestPage...");
Get.toNamed('/OrderRequestPage', arguments: {
'myListString': jsonEncode(driverList),
@@ -398,6 +417,10 @@ class LocationController extends GetxController with WidgetsBindingObserver {
void _startHeartbeat() {
_socketHeartbeat?.cancel();
_socketHeartbeat = Timer.periodic(const Duration(seconds: 25), (timer) {
// [Fix 6] تخطي الإرسال إذا كان stream الموقع نشطاً.
// الـ _locSub يرسل update_location عند كل تحرك (كل 5-10 ثوانٍ) تلقائياً.
// الـ heartbeat يكون مفيداً فقط عندما يتوقف الـ stream (الجهاز ثابت أو أوقف الخدمة).
if (_locSub != null) return;
if (socket != null && isSocketConnected && myLocation.latitude != 0) {
emitLocationToSocket(myLocation, heading, speed);
}
@@ -491,6 +514,10 @@ class LocationController extends GetxController with WidgetsBindingObserver {
speed = loc.speed ?? 0.0;
heading = loc.heading ?? 0.0;
box.write('last_lat', pos.latitude);
box.write('last_lng', pos.longitude);
box.write('last_heading', heading);
if (_lastPosForDistance != null) {
final d = _calculateDistance(_lastPosForDistance!, pos);
if (d > 5.0) totalDistance += d;
@@ -499,10 +526,25 @@ class LocationController extends GetxController with WidgetsBindingObserver {
update();
emitLocationToSocket(pos, heading, speed);
if (Get.isRegistered<HomeCaptainController>()) {
final homeCtrl = Get.find<HomeCaptainController>();
if (homeCtrl.isActive &&
homeCtrl.mapHomeCaptainController != null &&
homeCtrl.isHomeMapActive &&
homeCtrl.isMapReadyForCommands) {
homeCtrl.mapHomeCaptainController?.animateCamera(
CameraUpdate.newLatLngZoom(pos, 17.5),
);
}
}
await _saveBehaviorIfMoved(pos, now, currentSpeed: speed);
}, onError: (e) => Log.print('❌ Location Stream Error: $e'));
}
Timer? _socketWatchdogTimer;
Future<void> stopLocationUpdates() async {
Log.print("🛑 Stopping Location Updates...");
@@ -511,11 +553,11 @@ class LocationController extends GetxController with WidgetsBindingObserver {
_recordTimer?.cancel();
_uploadBatchTimer?.cancel();
_socketHeartbeat?.cancel();
_socketWatchdogTimer?.cancel();
if (socket != null) {
socket!.clearListeners();
socket!
.dispose(); // استخدام dispose بدلاً من disconnect لضمان تحرير الموارد على iOS
socket!.dispose();
}
if (!Platform.isIOS) {
@@ -534,6 +576,7 @@ class LocationController extends GetxController with WidgetsBindingObserver {
void _startBatchTimers() {
_recordTimer?.cancel();
_uploadBatchTimer?.cancel();
_socketWatchdogTimer?.cancel();
final recDur =
_isPowerSavingMode ? recordIntervalPowerSave : recordIntervalNormal;
@@ -544,6 +587,14 @@ class LocationController extends GetxController with WidgetsBindingObserver {
_recordTimer =
Timer.periodic(recDur, (_) => _recordCurrentLocationToBuffer());
_uploadBatchTimer = Timer.periodic(upDur, (_) => _flushBufferToServer());
// محاولة إعادة الاتصال بالسوكيت إذا انقطع كل 3 ثواني
_socketWatchdogTimer = Timer.periodic(const Duration(seconds: 3), (_) {
if (!isSocketConnected && !_isInitializingSocket) {
Log.print("🔄 Socket Watchdog: Attempting to reconnect socket...");
initSocket();
}
});
}
void _recordCurrentLocationToBuffer() {
@@ -736,7 +787,30 @@ class LocationController extends GetxController with WidgetsBindingObserver {
Future<LocationData?> getLocation() async {
try {
if (await _ensureServiceAndPermission()) {
return await location.getLocation();
final locData = await location.getLocation();
if (locData != null && locData.latitude != null && locData.longitude != null) {
myLocation = LatLng(locData.latitude!, locData.longitude!);
heading = locData.heading ?? 0.0;
speed = locData.speed ?? 0.0;
box.write('last_lat', myLocation.latitude);
box.write('last_lng', myLocation.longitude);
box.write('last_heading', heading);
update();
if (Get.isRegistered<HomeCaptainController>()) {
final homeCtrl = Get.find<HomeCaptainController>();
if (homeCtrl.mapHomeCaptainController != null &&
homeCtrl.isMapReadyForCommands) {
Log.print("📍 [LocationController] Animating camera to single location update");
homeCtrl.mapHomeCaptainController?.animateCamera(
CameraUpdate.newLatLngZoom(myLocation, 17.5),
);
}
}
}
return locData;
}
} catch (e) {
Log.print('❌ FAILED to get single location: $e');