diff --git a/android/app/build.gradle b/android/app/build.gradle index e88811b..8fc02fb 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -44,8 +44,8 @@ android { applicationId = "com.intaleq_driver" minSdk = 29 targetSdk = 36 - versionCode = 16 // I've used the higher version number from your first file - versionName = '1.0.16' // I've used the higher version name + versionCode = 17 // I've used the higher version number from your first file + versionName = '1.0.17' // I've used the higher version name multiDexEnabled = true ndk { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b843fc9..6c1228e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -49,12 +49,12 @@ android:value="2" /> - + android:value="main.dart" /> --> - + - - + android:exported="false" /> --> + diff --git a/bubble-master/lib/bubble.dart b/bubble-master/lib/bubble.dart index 00fa44a..eb5ce12 100755 --- a/bubble-master/lib/bubble.dart +++ b/bubble-master/lib/bubble.dart @@ -21,7 +21,7 @@ class Bubble { /// puts app in background and shows floaty-bubble head Future startBubbleHead({bool sendAppToBackground = true}) async { ByteData bytes = await rootBundle.load( - 'assets/images/s.png', + 'assets/images/logo.png', ); var buffer = bytes.buffer; var encodedImage = base64.encode(Uint8List.view(buffer)); diff --git a/lib/constant/links.dart b/lib/constant/links.dart index ca5882f..8a6c161 100755 --- a/lib/constant/links.dart +++ b/lib/constant/links.dart @@ -8,8 +8,7 @@ import 'box_name.dart'; class AppLink { static String serverPHP = box.read('serverPHP'); - static String seferPaymentServer = - 'https://walletintaleq.intaleq.xyz/v1/main'; + static String paymentServer = 'https://walletintaleq.intaleq.xyz/v1/main'; static String seferPaymentServer0 = 'https://walletintaleq.intaleq.xyz/v1/main'; @@ -26,7 +25,7 @@ class AppLink { static String loginJwtDriver = "$server/loginJwtDriver.php"; static String loginJwtWalletDriver = - "$seferPaymentServer/loginJwtWalletDriver.php"; + "$paymentServer/loginJwtWalletDriver.php"; static String loginFirstTimeDriver = "$server/loginFirstTimeDriver.php"; static String googleMapsLink = 'https://maps.googleapis.com/maps/api/'; @@ -48,25 +47,25 @@ class AppLink { static String addTokens = "$ride/firebase/add.php"; static String addTokensDriver = "$ride/firebase/addDriver.php"; static String addTokensDriverWallet = - "$seferPaymentServer/ride/firebase/addDriver.php"; + "$paymentServer/ride/firebase/addDriver.php"; //=======================Wallet=================== - static String wallet = '$seferPaymentServer/ride/passengerWallet'; - static String walletDriver = '$seferPaymentServer/ride/driverWallet'; + static String wallet = '$paymentServer/ride/passengerWallet'; + static String walletDriver = '$paymentServer/ride/driverWallet'; static String getAllPassengerTransaction = "$wallet/getAllPassengerTransaction.php"; static String payWithMTNConfirm = - "$seferPaymentServer/ride/mtn/driver/confirm_payment.php"; + "$paymentServer/ride/mtn/driver/confirm_payment.php"; static String payWithMTNStart = - "$seferPaymentServer/ride/mtn/driver/mtn_start.php"; + "$paymentServer/ride/mtn/driver/mtn_start.php"; static String payWithSyriatelConfirm = - "$seferPaymentServer/ride/syriatel/driver/confirm_payment.php"; + "$paymentServer/ride/syriatel/driver/confirm_payment.php"; static String payWithSyriatelStart = - "$seferPaymentServer/ride/syriatel/driver/start_payment.php"; + "$paymentServer/ride/syriatel/driver/start_payment.php"; static String payWithEcashDriver = - "$seferPaymentServer/ride/ecash/driver/payWithEcash.php"; + "$paymentServer/ride/ecash/driver/payWithEcash.php"; static String payWithEcashPassenger = - "$seferPaymentServer/ride/ecash/passenger/payWithEcash.php"; + "$paymentServer/ride/ecash/passenger/payWithEcash.php"; // wl.tripz-egypt.com/v1/main/ride/ecash/driver static String getWalletByPassenger = "$wallet/getWalletByPassenger.php"; static String getPassengersWallet = "$wallet/get.php"; @@ -136,35 +135,34 @@ class AppLink { static String addKazanPercent = "$ride/kazan/add.php"; ////-----------------DriverPayment------------------ - static String addDrivePayment = "$seferPaymentServer/ride/payment/add.php"; + static String addDrivePayment = "$paymentServer/ride/payment/add.php"; static String payWithPayMobCardDriver = - "$seferPaymentServer/ride/payMob/paymob_driver/payWithCard.php"; + "$paymentServer/ride/payMob/paymob_driver/payWithCard.php"; static String payWithWallet = - "$seferPaymentServer/ride/payMob/paymob_driver/payWithWallet.php"; + "$paymentServer/ride/payMob/paymob_driver/payWithWallet.php"; static String paymetVerifyDriver = - "$seferPaymentServer/ride/payMob/paymob_driver/paymet_verfy.php"; + "$paymentServer/ride/payMob/paymob_driver/paymet_verfy.php"; static String updatePaymetToPaid = - "$seferPaymentServer/ride/payment/updatePaymetToPaid.php"; + "$paymentServer/ride/payment/updatePaymetToPaid.php"; static String paymobPayoutDriverWallet = - "$seferPaymentServer/ride/payMob/paymob_driver/paymob_payout.php'"; + "$paymentServer/ride/payMob/paymob_driver/paymob_payout.php'"; - static String addSeferWallet = "$seferPaymentServer/ride/seferWallet/add.php"; - static String getSeferWallet = "$seferPaymentServer/ride/seferWallet/get.php"; + static String addSeferWallet = "$paymentServer/ride/seferWallet/add.php"; + static String getSeferWallet = "$paymentServer/ride/seferWallet/get.php"; static String addDriverPaymentPoints = - "$seferPaymentServer/ride/driverPayment/add.php"; + "$paymentServer/ride/driverPayment/add.php"; static String addPaymentTokenDriver = - "$seferPaymentServer/ride/driverWallet/addPaymentToken.php"; //driverWallet/addPaymentToken.php + "$paymentServer/ride/driverWallet/addPaymentToken.php"; //driverWallet/addPaymentToken.php static String addPaymentTokenPassenger = - "$seferPaymentServer/ride/passengerWallet/addPaymentTokenPassenger.php"; + "$paymentServer/ride/passengerWallet/addPaymentTokenPassenger.php"; static String getDriverPaymentPoints = - "$seferPaymentServer/ride/driverWallet/get.php"; - static String getDriverPaymentToday = - "$seferPaymentServer/ride/payment/get.php"; + "$paymentServer/ride/driverWallet/get.php"; + static String getDriverPaymentToday = "$paymentServer/ride/payment/get.php"; static String getCountRide = "$ride/payment/getCountRide.php"; static String getAllPaymentFromRide = - "$seferPaymentServer/ride/payment/getAllPayment.php"; + "$paymentServer/ride/payment/getAllPayment.php"; static String getAllPaymentVisa = - "$seferPaymentServer/ride/payment/getAllPaymentVisa.php"; + "$paymentServer/ride/payment/getAllPaymentVisa.php"; //-----------------Passenger NotificationCaptain------------------ static String addNotificationPassenger = diff --git a/lib/controller/auth/captin/login_captin_controller.dart b/lib/controller/auth/captin/login_captin_controller.dart index cb5158a..898e757 100755 --- a/lib/controller/auth/captin/login_captin_controller.dart +++ b/lib/controller/auth/captin/login_captin_controller.dart @@ -325,10 +325,8 @@ class LoginDriverController extends GetxController { key: BoxName.fingerPrint, value: fingerPrint.toString()); // print(jsonDecode(token)['data'][0]['token'].toString()); // print(box.read(BoxName.tokenDriver).toString()); - if (box.read(BoxName.emailDriver).toString() == - '963992952235@intaleqapp.com') { - Get.offAll(() => HomeCaptain()); - } + // if (box.read(BoxName.emailDriver).toString() != + // '963992952235@intaleqapp.com') { if (token != 'failure') { if ((jsonDecode(token)['data'][0]['token'].toString()) != box.read(BoxName.tokenDriver).toString()) { @@ -353,6 +351,7 @@ class LoginDriverController extends GetxController { }, ); } + // } } Get.offAll(() => HomeCaptain()); @@ -379,7 +378,7 @@ class LoginDriverController extends GetxController { logintest(String driverID, email) async { isloading = true; update(); - await SecurityHelper.performSecurityChecks(); + // await SecurityHelper.performSecurityChecks(); // Log.print('(BoxName.emailDriver): ${box.read(BoxName.emailDriver)}'); var res = await CRUD().get(link: AppLink.loginFromGoogleCaptin, payload: { @@ -388,75 +387,77 @@ class LoginDriverController extends GetxController { }); // print('res is $res'); - if (res == 'failure') { - await isPhoneVerified(); - // Get.snackbar('Failure', '', backgroundColor: Colors.red); - } else { - var jsonDecoeded = jsonDecode(res); - var d = jsonDecoeded['data'][0]; - if (jsonDecoeded.isNotEmpty) { - if (jsonDecoeded['status'] == 'success' && - d['is_verified'].toString() == '1') { - box.write(BoxName.emailDriver, d['email']); - box.write(BoxName.firstTimeLoadKey, 'false'); - box.write(BoxName.driverID, (d['id'])); - box.write(BoxName.isTest, '1'); - box.write(BoxName.gender, (d['gender'])); - box.write(BoxName.phoneVerified, d['is_verified'].toString()); - box.write(BoxName.phoneDriver, (d['phone'])); - box.write(BoxName.is_claimed, d['is_claimed']); - box.write(BoxName.isInstall, d['isInstall']); - // box.write( - // BoxName.isGiftToken, d['isGiftToken']); - box.write(BoxName.nameArabic, (d['name_arabic'])); - box.write(BoxName.carYear, d['year']); - box.write(BoxName.bankCodeDriver, (d['bankCode'])); - box.write(BoxName.accountBankNumberDriver, (d['accountBank'])); - box.write( - BoxName.nameDriver, - '${(d['first_name'])}' - ' ${(d['last_name'])}'); - if (((d['model']).toString().contains('دراجه') || - d['make'].toString().contains('دراجه '))) { - if ((d['gender']).toString() == 'Male') { - box.write(BoxName.carTypeOfDriver, 'Scooter'); - } else { - box.write(BoxName.carTypeOfDriver, 'Pink Bike'); - } - } else if (int.parse(d['year'].toString()) > 2016) { - if (d['gender'].toString() != 'Male') { - box.write(BoxName.carTypeOfDriver, 'Lady'); - } else { - box.write(BoxName.carTypeOfDriver, 'Comfort'); - } - } else if (int.parse(d['year'].toString()) > 2002 && - int.parse(d['year'].toString()) < 2016) { - box.write(BoxName.carTypeOfDriver, 'Speed'); - } else if (int.parse(d['year'].toString()) < 2002) { - box.write(BoxName.carTypeOfDriver, 'Awfar Car'); + // if (res == 'failure') { + // await isPhoneVerified(); + // // Get.snackbar('Failure', '', backgroundColor: Colors.red); + // } else + // { + var jsonDecoeded = jsonDecode(res); + var d = jsonDecoeded['data'][0]; + if (jsonDecoeded.isNotEmpty) { + if (jsonDecoeded['status'] == 'success' && + d['is_verified'].toString() == '1') { + box.write(BoxName.emailDriver, d['email']); + box.write(BoxName.firstTimeLoadKey, 'false'); + box.write(BoxName.driverID, (d['id'])); + box.write(BoxName.isTest, '1'); + box.write(BoxName.gender, (d['gender'])); + box.write(BoxName.phoneVerified, d['is_verified'].toString()); + box.write(BoxName.phoneDriver, (d['phone'])); + box.write(BoxName.is_claimed, d['is_claimed']); + box.write(BoxName.isInstall, d['isInstall']); + // box.write( + // BoxName.isGiftToken, d['isGiftToken']); + box.write(BoxName.nameArabic, (d['name_arabic'])); + box.write(BoxName.carYear, d['year']); + box.write(BoxName.bankCodeDriver, (d['bankCode'])); + box.write(BoxName.accountBankNumberDriver, (d['accountBank'])); + box.write( + BoxName.nameDriver, + '${(d['first_name'])}' + ' ${(d['last_name'])}'); + if (((d['model']).toString().contains('دراجه') || + d['make'].toString().contains('دراجه '))) { + if ((d['gender']).toString() == 'Male') { + box.write(BoxName.carTypeOfDriver, 'Scooter'); + } else { + box.write(BoxName.carTypeOfDriver, 'Pink Bike'); } - updateAppTester(AppInformation.appName); - - var token = await CRUD().get( - link: AppLink.getDriverToken, - payload: {'captain_id': (box.read(BoxName.driverID)).toString()}); - - String fingerPrint = await DeviceHelper.getDeviceFingerprint(); - await storage.write( - key: BoxName.fingerPrint, value: fingerPrint.toString()); - - Get.off(() => HomeCaptain()); - } else { - Get.offAll(() => PhoneNumberScreen()); - - isloading = false; - update(); + } else if (int.parse(d['year'].toString()) > 2016) { + if (d['gender'].toString() != 'Male') { + box.write(BoxName.carTypeOfDriver, 'Lady'); + } else { + box.write(BoxName.carTypeOfDriver, 'Comfort'); + } + } else if (int.parse(d['year'].toString()) > 2002 && + int.parse(d['year'].toString()) < 2016) { + box.write(BoxName.carTypeOfDriver, 'Speed'); + } else if (int.parse(d['year'].toString()) < 2002) { + box.write(BoxName.carTypeOfDriver, 'Awfar Car'); } - } else { - mySnackbarSuccess(''); + // updateAppTester(AppInformation.appName); - isloading = false; - update(); + // var token = await CRUD().get( + // link: AppLink.getDriverToken, + // payload: {'captain_id': (box.read(BoxName.driverID)).toString()}); + + // String fingerPrint = await DeviceHelper.getDeviceFingerprint(); + // await storage.write( + // key: BoxName.fingerPrint, value: fingerPrint.toString()); + + Get.off(() => HomeCaptain()); + // } else { + // Get.offAll(() => PhoneNumberScreen()); + + // isloading = false; + // update(); + // } + // } + // else { + // mySnackbarSuccess(''); + + // isloading = false; + // update(); } } } diff --git a/lib/controller/auth/captin/opt_token_controller.dart b/lib/controller/auth/captin/opt_token_controller.dart index 5250026..5c1bea4 100644 --- a/lib/controller/auth/captin/opt_token_controller.dart +++ b/lib/controller/auth/captin/opt_token_controller.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:get/get.dart'; +import 'package:sefer_driver/print.dart'; import 'package:sefer_driver/views/home/Captin/home_captain/home_captin.dart'; import '../../../constant/box_name.dart'; @@ -87,10 +88,10 @@ class OtpVerificationController extends GetxController { ); if (response != 'failure') { + Log.print('response: ${response}'); Get.back(); // توجه إلى الصفحة التالية await CRUD().post( - link: - '${AppLink.seferPaymentServer}/auth/token/update_driver_auth.php', + link: '${AppLink.paymentServer}/auth/token/update_driver_auth.php', payload: { 'token': box.read(BoxName.tokenDriver).toString(), 'fingerPrint': finger.toString(), diff --git a/lib/controller/auth/captin/phone_helper_controller.dart b/lib/controller/auth/captin/phone_helper_controller.dart index b6bc571..3bdb756 100644 --- a/lib/controller/auth/captin/phone_helper_controller.dart +++ b/lib/controller/auth/captin/phone_helper_controller.dart @@ -30,7 +30,7 @@ class PhoneAuthHelper { final data = (response); Log.print('data: ${data}'); // if (data['status'] == 'success') { - mySnackbarSuccess('An OTP has been sent to your WhatsApp number.'.tr); + mySnackbarSuccess('An OTP has been sent to your number.'.tr); return true; // } else { // mySnackeBarError(data['message'] ?? 'Failed to send OTP.'); diff --git a/lib/controller/firebase/firbase_messge.dart b/lib/controller/firebase/firbase_messge.dart index ab76893..dfdd07a 100755 --- a/lib/controller/firebase/firbase_messge.dart +++ b/lib/controller/firebase/firbase_messge.dart @@ -85,9 +85,9 @@ class FirebaseMessagesController extends GetxController { if (message.data.isNotEmpty && message.notification != null) { fireBaseTitles(message); } - if (message.data.isNotEmpty && message.notification != null) { - fireBaseTitles(message); - } + // if (message.data.isNotEmpty && message.notification != null) { + // fireBaseTitles(message); + // } }); FirebaseMessaging.onBackgroundMessage((RemoteMessage message) async {}); @@ -387,33 +387,12 @@ class FirebaseMessagesController extends GetxController { // })); // } - Future passengerDialog(String message) { - return Get.defaultDialog( - barrierDismissible: false, - title: 'message From passenger'.tr, - titleStyle: AppStyle.title, - middleTextStyle: AppStyle.title, - middleText: message.tr, - confirm: MyElevatedButton( - title: 'Ok'.tr, - onPressed: () { - // FirebaseMessagesController().sendNotificationToPassengerToken( - // 'Hi ,I will go now'.tr, - // 'I will go now'.tr, - // Get.find().driverToken, []); - // Get.find() - // .startTimerDriverWaitPassenger5Minute(); - - Get.back(); - })); - } - late String serviceAccountKeyJson; @override Future onInit() async { super.onInit(); try { - getToken(); + // getToken(); var encryptedKey = Env.privateKeyFCM; // Log.print('encryptedKey: ${encryptedKey}'); serviceAccountKeyJson = @@ -425,67 +404,84 @@ class FirebaseMessagesController extends GetxController { } void sendNotificationAll(String title, body, tone) async { - // Get the token you want to subtract. - String token = box.read(BoxName.tokenFCM); - tokens = box.read(BoxName.tokens); - // Subtract the token from the list of tokens. - tokens.remove(token); + // توكني الحالي (لا أرسل لنفسي) + final String myToken = box.read(BoxName.tokenFCM) ?? ''; + // اقرأ قائمة كل التوكنات + final List all = + List.from(box.read(BoxName.tokens) ?? const []); - // Save the list of tokens back to the box. - // box.write(BoxName.tokens, tokens); - tokens = box.read(BoxName.tokens); - for (var i = 0; i < tokens.length; i++) { - if (serviceAccountKeyJson.isEmpty) { - print("🔴 Error: Service Account Key is empty"); - return; - } - // Initialize AccessTokenManager - final accessTokenManager = AccessTokenManager(serviceAccountKeyJson); + // استبعد توكنك واحذف الفارغ + final targets = all.where((t) => t.isNotEmpty && t != myToken).toList(); - // Obtain an OAuth 2.0 access token - final accessToken = await accessTokenManager.getAccessToken(); - // Log.print('accessToken: ${accessToken}'); + if (serviceAccountKeyJson.isEmpty) { + print("🔴 Error: Service Account Key is empty"); + return; + } + final accessTokenManager = AccessTokenManager(serviceAccountKeyJson); + final accessToken = await accessTokenManager.getAccessToken(); - // Send the notification - final response = await http - .post( - Uri.parse( - 'https://fcm.googleapis.com/v1/projects/ride-b1bd8/messages:send'), - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer $accessToken', + for (final t in targets) { + // ⚠️ المهم: استخدم t (توكن الهدف)، وليس المتغير myToken + final response = await http.post( + Uri.parse( + 'https://fcm.googleapis.com/v1/projects/ride-b1bd8/messages:send'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $accessToken', + }, + body: jsonEncode({ + 'message': { + 'token': t, + 'notification': {'title': title, 'body': body}, + 'android': { + 'priority': 'HIGH', // القيم الصحيحة: HIGH/NORMAL + 'notification': {'sound': tone}, + // (اختياري) TTL لتجنّب رسائل قديمة + 'ttl': '30s', }, - body: jsonEncode({ - 'message': { - 'token': token, - 'notification': { - 'title': title, - 'body': body, - }, - // 'data': { - // 'DriverList': jsonEncode(data), - // }, - 'android': { - 'priority': 'high', // Set priority to high - 'notification': { - 'sound': tone, - }, - }, - 'apns': { - 'headers': { - 'apns-priority': '10', // Set APNs priority to 10 - }, - 'payload': { - 'aps': { - 'sound': tone, - }, - }, - }, + 'apns': { + 'headers': { + 'apns-priority': '10', + // لو iOS: حدد نوع الدفع + 'apns-push-type': 'alert', }, - }), - ) - .whenComplete(() {}) - .catchError((e) {}); + 'payload': { + 'aps': {'sound': tone} + }, + }, + }, + }), + ); + + if (response.statusCode != 200) { + // حاول تقرأ الخطأ وتشيل التوكنات التالفة + _handleV1Error(response, badToken: t); + await Future.delayed(const Duration(milliseconds: 50)); // تخفيف ضغط + } + } + } + + void _handleV1Error(http.Response res, {required String badToken}) { + try { + final body = jsonDecode(res.body); + final err = body['error']?['status']?.toString() ?? ''; + // أمثلة شائعة: + if (err.contains('UNREGISTERED') || err.contains('NOT_FOUND')) { + removeInvalidToken(badToken); + } else if (err.contains('INVALID_ARGUMENT')) { + // payload غير صحيح + print( + '⚠️ INVALID_ARGUMENT for $badToken: ${body['error']?['message']}'); + } else if (err.contains('RESOURCE_EXHAUSTED') || + err.contains('QUOTA_EXCEEDED')) { + // تجاوزت الحصة—خفّف السرعة/قسّم الإرسال (FCM v1 له حصة/دقيقة) + // https docs: 600k req/min per project (token bucket) + print('⏳ Throttled by FCM: slow down sending rate.'); + } else { + print('FCM v1 error: ${res.statusCode} ${res.body}'); + } + } catch (_) { + print('FCM v1 error: ${res.statusCode} ${res.body}'); } } @@ -523,7 +519,7 @@ class FirebaseMessagesController extends GetxController { 'passengerList': jsonEncode(map), }, 'android': { - 'priority': 'high', // Set priority to high + 'priority': 'HIGH ', // Set priority to high 'notification': { 'sound': tone, }, @@ -605,7 +601,7 @@ class FirebaseMessagesController extends GetxController { 'passengerList': jsonEncode(map), }, 'android': { - 'priority': 'high', // Set priority to high + 'priority': 'HIGH ', // Set priority to high 'notification': { 'sound': tone, }, @@ -692,7 +688,7 @@ class FirebaseMessagesController extends GetxController { 'DriverList': jsonEncode(data), }, 'android': { - 'priority': 'high', // Set priority to high + 'priority': 'HIGH ', // Set priority to high 'notification': { 'sound': tone, }, diff --git a/lib/controller/firebase/notification_service.dart b/lib/controller/firebase/notification_service.dart new file mode 100644 index 0000000..ecb3000 --- /dev/null +++ b/lib/controller/firebase/notification_service.dart @@ -0,0 +1,71 @@ +import 'package:http/http.dart' as http; +import 'dart:convert'; + +class NotificationService { + // استبدل هذا الرابط بالرابط الصحيح لملف PHP على السيرفر الخاص بك + static const String _serverUrl = + 'https://syria.intaleq.xyz/intaleq/fcm/send_fcm.php'; + + /// Sends a notification via your backend server. + /// + /// [target]: The device token or the topic name. + /// [title]: The notification title. + /// [body]: The notification body. + /// [isTopic]: Set to true if the target is a topic, false if it's a device token. + static Future sendNotification({ + required String target, + required String title, + required String body, + bool isTopic = false, + }) async { + try { + final response = await http.post( + Uri.parse(_serverUrl), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: jsonEncode({ + 'target': target, + 'title': title, + 'body': body, + 'isTopic': isTopic, + }), + ); + + if (response.statusCode == 200) { + print('Notification sent successfully.'); + print('Server Response: ${response.body}'); + } else { + print( + 'Failed to send notification. Status code: ${response.statusCode}'); + print('Server Error: ${response.body}'); + } + } catch (e) { + print('An error occurred while sending notification: $e'); + } + } +} + +// --- Example of how to use it --- + +// To send to a specific driver (using their token) +void sendToSpecificDriver() { + String driverToken = + 'bk3RNwTe3H0:CI2k_HHwgIpoDKCI5oT...'; // The driver's FCM token + NotificationService.sendNotification( + target: driverToken, + title: 'New Trip Request!', + body: 'A passenger is waiting for you.', + isTopic: false, // Important: this is a token + ); +} + +// To send to all drivers (using a topic) +void sendToAllDrivers() { + NotificationService.sendNotification( + target: 'drivers', // The name of the topic + title: 'Important Announcement', + body: 'Please update your app to the latest version.', + isTopic: true, // Important: this is a topic + ); +} diff --git a/lib/controller/functions/crud.dart b/lib/controller/functions/crud.dart index 6cdfe55..d42adee 100755 --- a/lib/controller/functions/crud.dart +++ b/lib/controller/functions/crud.dart @@ -108,8 +108,6 @@ class CRUD { final sc = response.statusCode; final body = response.body; - Log.print('body: ${body}'); - Log.print('body: ${body}'); // 2xx if (sc >= 200 && sc < 300) { @@ -193,6 +191,9 @@ class CRUD { 'Bearer ${r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]}' }, ); + // Log.print('response: ${response.body}'); + // Log.print('req: ${response.request}'); + // Log.print('payload: ${payload}'); if (response.statusCode == 200) { var jsonData = jsonDecode(response.body); @@ -263,9 +264,6 @@ class CRUD { 'X-HMAC-Auth': hmac.toString(), }, ); - // Log.print('response.request: ${response.request}'); - // Log.print('response.body: ${response.body}'); - // Log.print('response.payload: ${payload}'); if (response.statusCode == 200) { var jsonData = jsonDecode(response.body); diff --git a/lib/controller/home/captin/home_captain_controller.dart b/lib/controller/home/captin/home_captain_controller.dart index f0ba3fe..42bc257 100755 --- a/lib/controller/home/captin/home_captain_controller.dart +++ b/lib/controller/home/captin/home_captain_controller.dart @@ -336,7 +336,7 @@ class HomeCaptainController extends GetxController { CRUD().post(link: AppLink.addTokensDriver, payload: payload); await CRUD().post( - link: "${AppLink.seferPaymentServer}/ride/firebase/addDriver.php", + link: "${AppLink.paymentServer}/ride/firebase/addDriver.php", payload: payload); // MapDriverController().driverCallPassenger(); // box.write(BoxName.statusDriverLocation, 'off'); diff --git a/lib/controller/home/captin/map_driver_controller.dart b/lib/controller/home/captin/map_driver_controller.dart index 7963228..e3147a5 100755 --- a/lib/controller/home/captin/map_driver_controller.dart +++ b/lib/controller/home/captin/map_driver_controller.dart @@ -335,7 +335,7 @@ class MapDriverController extends GetxController { // Get.find().changeToAppliedRide('Applied'); Get.find().sendNotificationToDriverMAP( - 'Driver Is Going To Passenger'.tr, + 'Driver Is Going To Passenger', box.read(BoxName.nameDriver).toString(), //todo name driver tokenPassenger, [], @@ -431,7 +431,7 @@ class MapDriverController extends GetxController { ? Get.find() : Get.put(FirebaseMessagesController()); fcm.sendNotificationToDriverMAP( - 'Trip is Begin'.tr, + 'Trip is Begin', box.read(BoxName.nameDriver).toString(), tokenPassenger, [], @@ -732,8 +732,7 @@ class MapDriverController extends GetxController { )); apiCalls.add(CRUD().postWallet( - link: - "${AppLink.seferPaymentServer}/ride/payment/process_ride_payments.php", + link: "${AppLink.paymentServer}/ride/payment/process_ride_payments.php", payload: paymentProcessingPayload, )); @@ -754,7 +753,7 @@ class MapDriverController extends GetxController { .sendSummaryToServer(driverId, rideId); Get.find().sendNotificationToDriverMAP( - "Driver Finish Trip".tr, + "Driver Finish Trip", '${'you will pay to Driver'.tr} $paymentAmount \$', tokenPassenger, [ @@ -1619,7 +1618,7 @@ class MapDriverController extends GetxController { if (distance < 300) { // 300 متر قبل الوجهة Get.find().sendNotificationToDriverMAP( - "You are near the destination".tr, + "You are near the destination", "You are near the destination".tr, tokenPassenger, [ diff --git a/lib/controller/home/captin/order_request_controller.dart b/lib/controller/home/captin/order_request_controller.dart index fbf71d7..b6451d9 100755 --- a/lib/controller/home/captin/order_request_controller.dart +++ b/lib/controller/home/captin/order_request_controller.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter_overlay_window/flutter_overlay_window.dart'; @@ -37,10 +38,13 @@ class OrderRequestController extends GetxController { Future onInit() async { print('OrderRequestController onInit called'); await initializeOrderPage(); - bool isOverlayActive = await FlutterOverlayWindow.isActive(); - if (isOverlayActive) { - await FlutterOverlayWindow.closeOverlay(); + if (Platform.isAndroid) { + bool isOverlayActive = await FlutterOverlayWindow.isActive(); + if (isOverlayActive) { + await FlutterOverlayWindow.closeOverlay(); + } } + addCustomStartIcon(); addCustomEndIcon(); startTimer( @@ -61,6 +65,7 @@ class OrderRequestController extends GetxController { Future initializeOrderPage() async { final myListString = Get.arguments['myListString']; + Log.print('myListString0000: ${myListString}'); if (Get.arguments['DriverList'] == null || Get.arguments['DriverList'].isEmpty) { diff --git a/lib/controller/home/navigation/decode_polyline_isolate.dart b/lib/controller/home/navigation/decode_polyline_isolate.dart new file mode 100644 index 0000000..98348dc --- /dev/null +++ b/lib/controller/home/navigation/decode_polyline_isolate.dart @@ -0,0 +1,41 @@ +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +// تم تعديل الدالة لتقبل وسيط من نوع `dynamic` لحل مشكلة عدم تطابق الأنواع مع دالة `compute`. +// هذه الدالة لا تزال تعمل كدالة من المستوى الأعلى (Top-level function) +// وهو شرط أساسي لاستخدامها مع دالة compute. +List decodePolylineIsolate(dynamic encodedMessage) { + // التأكد من أن الرسالة المستقبلة هي من نوع String + if (encodedMessage is! String) { + // إرجاع قائمة فارغة أو إظهار خطأ إذا كان النوع غير صحيح + return []; + } + final String encoded = encodedMessage; + + List points = []; + int index = 0, len = encoded.length; + int lat = 0, lng = 0; + + while (index < len) { + int b, shift = 0, result = 0; + do { + b = encoded.codeUnitAt(index++) - 63; + result |= (b & 0x1f) << shift; + shift += 5; + } while (b >= 0x20); + int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1)); + lat += dlat; + + shift = 0; + result = 0; + do { + b = encoded.codeUnitAt(index++) - 63; + result |= (b & 0x1f) << shift; + shift += 5; + } while (b >= 0x20); + int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1)); + lng += dlng; + + points.add(LatLng(lat / 1E5, lng / 1E5)); + } + return points; +} diff --git a/lib/controller/home/navigation/navigation_controller.dart b/lib/controller/home/navigation/navigation_controller.dart index 4963ab9..6faeec6 100644 --- a/lib/controller/home/navigation/navigation_controller.dart +++ b/lib/controller/home/navigation/navigation_controller.dart @@ -1,12 +1,11 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; -import 'dart:math' as math; +import 'package:flutter/foundation.dart'; // <<<--- إضافة مهمة لاستخدام دالة compute import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; -import 'package:google_polyline_algorithm/google_polyline_algorithm.dart'; import 'package:sefer_driver/constant/colors.dart'; import 'package:sefer_driver/env/env.dart'; @@ -16,6 +15,7 @@ import '../../../constant/links.dart'; import '../../../print.dart'; import '../../functions/crud.dart'; import '../../functions/tts.dart'; +import 'decode_polyline_isolate.dart'; class NavigationController extends GetxController { // --- متغيرات الحالة العامة --- @@ -319,24 +319,24 @@ class NavigationController extends GetxController { // ٤. دوال مساعدة وتجهيز البيانات // ======================================================================= - void _prepareStepData() { + // <<<--- التعديل الأول: تغيير الدالة لتكون async + Future _prepareStepData() async { _stepBounds.clear(); _stepPolylines.clear(); if (routeSteps.isEmpty) return; for (final step in routeSteps) { final pointsString = step['polyline']['points']; - final List> points = - decodePolyline(pointsString).cast>(); - final polylineCoordinates = points - .map((point) => LatLng(point[0].toDouble(), point[1].toDouble())) - .toList(); + // <<<--- التعديل الثاني: استخدام compute لفك التشفير في خيط منفصل + // وتصحيح طريقة التعامل مع القائمة المُرجعة + final List polylineCoordinates = await compute( + decodePolylineIsolate as ComputeCallback>, + pointsString); + _stepPolylines.add(polylineCoordinates); // تخزين نقاط الخطوة _stepBounds.add(_boundsFromLatLngList(polylineCoordinates)); } } - // ... باقي دوال الكنترولر بدون تغيير ... - // (selectDestination, onMapLongPressed, startNavigationTo, getRoute, etc.) Future selectDestination(dynamic place) async { placeDestinationController.clear(); placesDestination = []; @@ -401,11 +401,11 @@ class NavigationController extends GetxController { } Future getRoute(LatLng origin, LatLng destination) async { - final String key = Platform.isAndroid ? Env.mapAPIKEY : Env.mapAPIKEYIOS; + final String key = Env.mapAPIKEY; final url = '${AppLink.googleMapsLink}directions/json?language=ar&destination=${destination.latitude},${destination.longitude}&origin=${origin.latitude},${origin.longitude}&key=${key}&mode=driving'; var response = await CRUD().getGoogleApi(link: url, payload: {}); - Log.print('response: ${response}'); + // Log.print('response: ${response}'); if (response == null || response['routes'].isEmpty) { Get.snackbar('خطأ', 'لم يتم العثور على مسار.'); @@ -414,11 +414,11 @@ class NavigationController extends GetxController { polylines.clear(); final pointsString = response['routes'][0]['overview_polyline']['points']; - final List> points = - decodePolyline(pointsString).cast>(); - _fullRouteCoordinates = points - .map((point) => LatLng(point[0].toDouble(), point[1].toDouble())) - .toList(); + + // <<<--- التعديل الثالث: استخدام compute هنا أيضًا للمسار الرئيسي + _fullRouteCoordinates = await compute( + decodePolylineIsolate as ComputeCallback>, + pointsString); polylines.add( Polyline( @@ -441,7 +441,9 @@ class NavigationController extends GetxController { routeSteps = List>.from( response['routes'][0]['legs'][0]['steps']); - _prepareStepData(); + + // <<<--- التعديل الرابع: انتظار انتهاء الدالة بعد تحويلها إلى async + await _prepareStepData(); currentStepIndex = 0; _nextInstructionSpoken = false; @@ -531,57 +533,32 @@ class NavigationController extends GetxController { String _parseInstruction(String html) => html.replaceAll(RegExp(r'<[^>]*>'), ' '); - double _haversineKm(double lat1, double lon1, double lat2, double lon2) { - const R = 6371.0; // km - final dLat = (lat2 - lat1) * math.pi / 180.0; - final dLon = (lon2 - lon1) * math.pi / 180.0; - final a = math.sin(dLat / 2) * math.sin(dLat / 2) + - math.cos(lat1 * math.pi / 180.0) * - math.cos(lat2 * math.pi / 180.0) * - math.sin(dLon / 2) * - math.sin(dLon / 2); - final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)); - return R * c; - } - - /// تحويل نصف قطر بالكيلومتر إلى دلتا درجات عرض - double _kmToLatDelta(double km) => km / 111.0; - - /// تحويل نصف قطر بالكيلومتر إلى دلتا درجات طول (تعتمد على خط العرض) - double _kmToLngDelta(double km, double atLat) => - km / (111.320 * math.cos(atLat * math.pi / 180.0)).abs().clamp(1e-6, 1e9); - - /// حساب درجة التطابق النصي (كل كلمة تبدأ بها الاسم = 2 نقاط، يحتويها = 1 نقطة) - double _relevanceScore(String name, String query) { - final n = name.toLowerCase(); - final parts = - query.toLowerCase().split(RegExp(r'\s+')).where((p) => p.length >= 2); - double s = 0.0; - for (final p in parts) { - if (n.startsWith(p)) { - s += 2.0; - } else if (n.contains(p)) { - s += 1.0; - } - } - return s; - } + // ======================================================================= + // ٥. دالة البحث عن الأماكن المحدثة والدوال المساعدة لها + // ======================================================================= + /// الدالة المحدثة للبحث عن الأماكن Future getPlaces() async { final q = placeDestinationController.text.trim(); - if (q.isEmpty) { + if (q.isEmpty || q.length < 3) { placesDestination = []; update(); return; } + // التأكد من أن الموقع الحالي ليس null + if (myLocation == null) { + print('myLocation is null, cannot search for places.'); + return; + } + final lat = myLocation!.latitude; final lng = myLocation!.longitude; - // نصف قطر البحث بالكيلومتر (عدّل حسب رغبتك) + // نصف قطر البحث بالكيلومتر const radiusKm = 200.0; - // حساب الباوند الصحيح (درجات، وليس 2.2 درجة ثابتة) + // حساب النطاق الجغرافي (Bounding Box) لإرساله للسيرفر final latDelta = _kmToLatDelta(radiusKm); final lngDelta = _kmToLngDelta(radiusKm, lat); @@ -591,6 +568,7 @@ class NavigationController extends GetxController { final lngMax = lng + lngDelta; try { + // استدعاء الـ API final response = await CRUD().post( link: AppLink.getPlacesSyria, payload: { @@ -602,53 +580,57 @@ class NavigationController extends GetxController { }, ); - // يدعم شكلي استجابة: إما {"...","message":[...]} أو قائمة مباشرة [...] + // معالجة الاستجابة من السيرفر بشكل يوافق {"status":"success", "message":[...]} List list; - if (response is Map && response['message'] is List) { - list = List.from(response['message'] as List); + if (response is Map) { + if (response['status'] == 'success' && response['message'] is List) { + list = List.from(response['message'] as List); + } else if (response['status'] == 'failure') { + print('Server Error: ${response['message']}'); + return; + } else { + print('Unexpected Map shape from server'); + return; + } } else if (response is List) { + // للتعامل مع الحالات التي قد يرجع فيها السيرفر قائمة مباشرة list = List.from(response); } else { - print('Unexpected response shape'); + print('Unexpected response shape from server'); return; } - // جهّز الحقول المحتملة للأسماء + // دالة مساعدة لاختيار أفضل اسم متاح String _bestName(Map p) { - return (p['name'] ?? p['name_ar'] ?? p['name_en'] ?? '').toString(); + return (p['name_ar'] ?? p['name'] ?? p['name_en'] ?? '').toString(); } - // احسب المسافة ودرجة التطابق والنقاط + // حساب المسافة والصلة والنقاط النهائية لكل نتيجة for (final p in list) { - final plat = double.tryParse(p['latitude']?.toString() ?? '') ?? 0.0; - final plng = double.tryParse(p['longitude']?.toString() ?? '') ?? 0.0; + final plat = double.tryParse(p['latitude']?.toString() ?? '0.0') ?? 0.0; + final plng = + double.tryParse(p['longitude']?.toString() ?? '0.0') ?? 0.0; - final d = _haversineKm(lat, lng, plat, plng); - final rel = _relevanceScore(_bestName(p), q); + final distance = _haversineKm(lat, lng, plat, plng); + final relevance = _relevanceScore(_bestName(p), q); - // معادلة ترتيب ذكية: مسافة أقل + تطابق أعلى = نقاط أعلى - // تضيف +1 لضمان عدم وصول الوزن للصفر عند عدم وجود تطابق - final score = (1.0 / (1.0 + d)) * (1.0 + rel); + // معادلة الترتيب: (الأولوية للمسافة الأقرب) * (ثم الصلة الأعلى) + final score = (1.0 / (1.0 + distance)) * (1.0 + relevance); - p['distanceKm'] = d; - p['relevance'] = rel; + p['distanceKm'] = distance; + p['relevance'] = relevance; p['score'] = score; } - // رتّب حسب score تنازليًا، ثم المسافة تصاعديًا كحسم + // ترتيب القائمة النهائية حسب النقاط (الأعلى أولاً) list.sort((a, b) { final sa = (a['score'] ?? 0.0) as double; final sb = (b['score'] ?? 0.0) as double; - final cmp = sb.compareTo(sa); - if (cmp != 0) return cmp; - final da = (a['distanceKm'] ?? 1e9) as double; - final db = (b['distanceKm'] ?? 1e9) as double; - return da.compareTo(db); + return sb.compareTo(sa); }); - // خذ أول 10–15 للعرض (اختياري)، أو اعرض الكل - placesDestination = list.take(15).toList(); - Log.print('placesDestination: $placesDestination'); + placesDestination = list; + Log.print('Updated places: $placesDestination'); update(); } catch (e) { print('Exception in getPlaces: $e'); @@ -659,4 +641,44 @@ class NavigationController extends GetxController { if (_debounce?.isActive ?? false) _debounce!.cancel(); _debounce = Timer(const Duration(milliseconds: 700), () => getPlaces()); } + + // ----------------------------------------------------------------- + // --== دوال مساعدة (محدثة) ==-- + // ----------------------------------------------------------------- + + /// تحسب المسافة بين نقطتين بالكيلومتر (معادلة هافرساين) + double _haversineKm(double lat1, double lon1, double lat2, double lon2) { + const R = 6371.0; // نصف قطر الأرض بالكيلومتر + final dLat = (lat2 - lat1) * (pi / 180.0); + final dLon = (lon2 - lon1) * (pi / 180.0); + final rLat1 = lat1 * (pi / 180.0); + final rLat2 = lat2 * (pi / 180.0); + + final a = sin(dLat / 2) * sin(dLat / 2) + + cos(rLat1) * cos(rLat2) * sin(dLon / 2) * sin(dLon / 2); + final c = 2 * atan2(sqrt(a), sqrt(1 - a)); + return R * c; + } + + /// تحسب درجة تطابق بسيطة بين اسم المكان وكلمة البحث + double _relevanceScore(String placeName, String query) { + if (placeName.isEmpty || query.isEmpty) return 0.0; + final pLower = placeName.toLowerCase(); + final qLower = query.toLowerCase(); + if (pLower.startsWith(qLower)) return 1.0; // تطابق كامل في البداية + if (pLower.contains(qLower)) return 0.5; // تحتوي على الكلمة + return 0.0; + } + + /// تحويل كيلومتر إلى فرق درجات لخط العرض + double _kmToLatDelta(double km) { + const kmInDegree = 111.32; + return km / kmInDegree; + } + + /// تحويل كيلومتر إلى فرق درجات لخط الطول (يعتمد على خط العرض الحالي) + double _kmToLngDelta(double km, double latitude) { + const kmInDegree = 111.32; + return km / (kmInDegree * cos(latitude * (pi / 180.0))); + } } diff --git a/lib/controller/local/translations.dart b/lib/controller/local/translations.dart index 4ab672f..9538097 100755 --- a/lib/controller/local/translations.dart +++ b/lib/controller/local/translations.dart @@ -133,6 +133,8 @@ class MyTranslation extends Translations { "How to use Intaleq": "كيفية استخدام Intaleq", "What are the order details we provide to you?": "ما هي تفاصيل الطلب التي نوفرها لك؟", + 'An OTP has been sent to your number.': + 'تم إرسال رمز التحقق إلى رقمك.', "Intaleq Wallet Features:\n\nTransfer money multiple times.\nTransfer to anyone.\nMake purchases.\nCharge your account.\nCharge a friend's Intaleq account.\nStore your money with us and receive it in your bank as a monthly salary.": "ميزات محفظة Intaleq:\n\nتحويل الأموال عدة مرات.\nالتحويل إلى أي شخص.\nإجراء عمليات شراء.\nشحن حسابك.\nشحن حساب Intaleq لصديق.\nقم بتخزين أموالك معنا واستلامها في بنكك كراتب شهري.", "What is the feature of our wallet?": "ما هي مميزات محفظتنا؟", diff --git a/lib/controller/notification/ride_available_controller.dart b/lib/controller/notification/ride_available_controller.dart index cdbf324..69533ee 100755 --- a/lib/controller/notification/ride_available_controller.dart +++ b/lib/controller/notification/ride_available_controller.dart @@ -3,21 +3,20 @@ import 'dart:math'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; -import 'package:sefer_driver/constant/box_name.dart'; -import 'package:sefer_driver/controller/functions/location_controller.dart'; +import 'package:flutter/widgets.dart'; // Import for WidgetsBinding import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import '../../constant/links.dart'; -import '../../main.dart'; -import '../../print.dart'; -import '../../views/widgets/mydialoug.dart'; import '../functions/crud.dart'; +import '../functions/location_controller.dart'; class RideAvailableController extends GetxController { bool isLoading = false; - Map rideAvailableMap = {}; + // FIX 1: Initialize the map with a default structure. + // This prevents `rideAvailableMap['message']` from ever being null in the UI. + Map rideAvailableMap = {'message': []}; late LatLng southwest; late LatLng northeast; @@ -30,19 +29,15 @@ class RideAvailableController extends GetxController { double minLat = lat - latDelta; double maxLat = lat + latDelta; - double minLng = lng - lngDelta; double maxLng = lng + lngDelta; - // Ensure the latitude is between -90 and 90 minLat = max(-90.0, minLat); maxLat = min(90.0, maxLat); - // Ensure the longitude is between -180 and 180 minLng = (minLng + 180) % 360 - 180; maxLng = (maxLng + 180) % 360 - 180; - // Ensure the bounds are in the correct order if (minLng > maxLng) { double temp = minLng; minLng = maxLng; @@ -60,7 +55,6 @@ class RideAvailableController extends GetxController { double startLatitude = double.parse(startLocationParts[0]); double startLongitude = double.parse(startLocationParts[1]); - // Assuming currentLocation is the driver's location double currentLatitude = Get.find().myLocation.latitude; double currentLongitude = Get.find().myLocation.longitude; @@ -73,22 +67,28 @@ class RideAvailableController extends GetxController { ); } - // void sortRidesByDistance() { - // rideAvailableMap['message'].sort((a, b) { - // double distanceA = calculateDistance(a['start_location']); - // double distanceB = calculateDistance(b['start_location']); - // return distanceA.compareTo(distanceB); - // }); - // } + // A helper function to safely show dialogs after the build cycle is complete. + void _showDialogAfterBuild(Widget dialog) { + // FIX 2: Use addPostFrameCallback to ensure dialogs are shown after the build process. + // This resolves the "visitChildElements() called during build" error. + WidgetsBinding.instance.addPostFrameCallback((_) { + Get.dialog( + dialog, + barrierDismissible: true, + transitionCurve: Curves.easeOutBack, + transitionDuration: const Duration(milliseconds: 200), + ); + }); + } - getRideAvailable() async { + Future getRideAvailable() async { try { isLoading = true; update(); LatLngBounds bounds = calculateBounds( - Get.find().myLocation!.latitude, - Get.find().myLocation!.longitude, + Get.find().myLocation.latitude, + Get.find().myLocation.longitude, 4000); var payload = { @@ -101,85 +101,108 @@ class RideAvailableController extends GetxController { var res = await CRUD().get(link: AppLink.getRideWaiting, payload: payload); + isLoading = false; // Request is complete, stop loading indicator. + if (res != 'failure') { - rideAvailableMap = jsonDecode(res); - isLoading = false; - update(); + final decodedResponse = jsonDecode(res); + // Check for valid response structure + if (decodedResponse is Map && + decodedResponse.containsKey('message') && + decodedResponse['message'] is List) { + rideAvailableMap = decodedResponse; + // If the list of rides is empty, show the "No Rides" dialog + if ((rideAvailableMap['message'] as List).isEmpty) { + _showDialogAfterBuild(_buildNoRidesDialog()); + } + } else { + // If response format is unexpected, treat as no rides and show dialog + rideAvailableMap = {'message': []}; + _showDialogAfterBuild(_buildNoRidesDialog()); + } + update(); // Update the UI with new data (or empty list) } else { + // This block now handles network/server errors correctly HapticFeedback.lightImpact(); - Get.dialog( - CupertinoAlertDialog( - title: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - CupertinoIcons.car, - size: 44, - color: CupertinoColors.systemGrey, - ), - const SizedBox(height: 12), - Text( - "No Rides Available".tr, - style: const TextStyle( - fontSize: 17, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - content: Padding( - padding: const EdgeInsets.only(top: 8), - child: Text( - "Please check back later for available rides.".tr, - style: const TextStyle( - fontSize: 13, - color: CupertinoColors.systemGrey, - ), - ), - ), - actions: [ - CupertinoDialogAction( - onPressed: () { - Get.back(); - Get.back(); - }, - child: Text('OK'.tr), - ), - ], - ), - barrierDismissible: true, - transitionCurve: Curves.easeOutBack, - transitionDuration: const Duration(milliseconds: 200), - ); + update(); // Update UI to turn off loader + // Show a proper error dialog instead of "No Rides" + _showDialogAfterBuild( + _buildErrorDialog("Failed to fetch rides. Please try again.".tr)); } } catch (e) { isLoading = false; update(); - Get.dialog( - CupertinoAlertDialog( - title: const Icon( - CupertinoIcons.exclamationmark_triangle_fill, - color: CupertinoColors.systemRed, - size: 44, - ), - content: Text( - "Error fetching rides. Please try again.".tr, - style: const TextStyle(fontSize: 14), - ), - actions: [ - CupertinoDialogAction( - onPressed: () => Get.back(), - child: Text('OK'.tr), - ), - ], - ), - ); + // This catches other exceptions like JSON parsing errors + _showDialogAfterBuild( + _buildErrorDialog("An unexpected error occurred.".tr)); } } + // Extracted dialogs into builder methods for cleanliness. + Widget _buildNoRidesDialog() { + return CupertinoAlertDialog( + title: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + CupertinoIcons.car, + size: 44, + color: CupertinoColors.systemGrey, + ), + const SizedBox(height: 12), + Text( + "No Rides Available".tr, + style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600), + ), + ], + ), + content: Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + "Please check back later for available rides.".tr, + style: + const TextStyle(fontSize: 13, color: CupertinoColors.systemGrey), + ), + ), + actions: [ + CupertinoDialogAction( + onPressed: () { + Get.back(); // Close dialog + Get.back(); // Go back from AvailableRidesPage + }, + child: Text('OK'.tr), + ), + ], + ); + } + + Widget _buildErrorDialog(String error) { + // You can log the error here for debugging. + // print("Error fetching rides: $error"); + return CupertinoAlertDialog( + title: const Icon( + CupertinoIcons.exclamationmark_triangle_fill, + color: CupertinoColors.systemRed, + size: 44, + ), + content: Text( + error, // Display the specific error message passed to the function + style: const TextStyle(fontSize: 14), + ), + actions: [ + CupertinoDialogAction( + onPressed: () { + Get.back(); // Close dialog + Get.back(); // Go back from AvailableRidesPage + }, + child: Text('OK'.tr), + ), + ], + ); + } + @override void onInit() { - getRideAvailable(); super.onInit(); + getRideAvailable(); } } diff --git a/lib/controller/payment/mtn_new/mtn_payment_new_screen.dart b/lib/controller/payment/mtn_new/mtn_payment_new_screen.dart new file mode 100644 index 0000000..281c7b9 --- /dev/null +++ b/lib/controller/payment/mtn_new/mtn_payment_new_screen.dart @@ -0,0 +1,350 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +// import 'package:http/http.dart' as http; +import 'package:intl/intl.dart'; +import 'package:sefer_driver/constant/links.dart'; // افترض وجود هذا الملف +import 'package:sefer_driver/controller/functions/crud.dart'; // افترض وجود هذا الملف +import '../../../main.dart'; // افترض وجود box هنا +import '../../../constant/box_name.dart'; // افترض وجود هذا الملف + +// Service class to handle MTN payment logic +class MtnPaymentService { + final String _baseUrl = + "${AppLink.paymentServer}/ride/mtn_new"; // تأكد من تعديل المسار + + // Function to create a new invoice + Future createInvoice({ + required String userId, + required String userType, // 'driver' or 'passenger' + required double amount, + required String mtnPhone, + }) async { + final url = "$_baseUrl/create_mtn_invoice.php"; + try { + final response = await CRUD().postWallet( + // استخدام نفس دالة CRUD + link: url, + payload: { + 'user_id': userId, + 'user_type': userType, + 'amount': amount.toString(), + 'mtn_phone': mtnPhone, + }, + ).timeout(const Duration(seconds: 15)); + + if (response != 'failure') { + final data = response; + if (data['status'] == 'success' && data['invoice_number'] != null) { + debugPrint("MTN Invoice created: ${data['invoice_number']}"); + return data['invoice_number'].toString(); + } else { + debugPrint("Failed to create MTN invoice: ${data['message']}"); + return null; + } + } else { + debugPrint("Server error during MTN invoice creation."); + return null; + } + } catch (e) { + debugPrint("Exception during MTN invoice creation: $e"); + return null; + } + } + + // Function to check invoice status (polling) + Future checkInvoiceStatus(String invoiceNumber) async { + // This should point to a new script on your server that checks mtn_invoices table + final url = "$_baseUrl/check_mtn_invoice_status.php"; + try { + final response = await CRUD().postWallet(link: url, payload: { + 'invoice_number': invoiceNumber, + }).timeout(const Duration(seconds: 10)); + + if (response != 'failure') { + final data = response; + return data['status'] == 'success' && + data['invoice_status'] == 'completed'; + } + return false; + } catch (e) { + debugPrint("Error checking MTN invoice status: $e"); + return false; + } + } +} + +enum PaymentStatus { + creatingInvoice, + waitingForPayment, + paymentSuccess, + paymentTimeout, + paymentError +} + +class PaymentScreenMtn extends StatefulWidget { + final double amount; + // يمكنك إضافة متغير لتحديد هل المستخدم سائق أم راكب + final String userType; // 'driver' or 'passenger' + + const PaymentScreenMtn({ + super.key, + required this.amount, + required this.userType, + }); + + @override + _PaymentScreenMtnState createState() => _PaymentScreenMtnState(); +} + +class _PaymentScreenMtnState extends State { + final MtnPaymentService _paymentService = MtnPaymentService(); + Timer? _pollingTimer; + PaymentStatus _status = PaymentStatus.creatingInvoice; + String? _invoiceNumber; + // جلب البيانات من الـ box + final String userId = + box.read(BoxName.driverID) ?? box.read(BoxName.passengerID); + final String phone = box.read(BoxName.phoneWallet); + + @override + void initState() { + super.initState(); + _createAndPollInvoice(); + } + + @override + void dispose() { + _pollingTimer?.cancel(); + super.dispose(); + } + + void _createAndPollInvoice() async { + setState(() => _status = PaymentStatus.creatingInvoice); + + final invoiceNumber = await _paymentService.createInvoice( + userId: userId, + userType: widget.userType, + amount: widget.amount, + mtnPhone: phone, + ); + + if (invoiceNumber != null && mounted) { + setState(() { + _invoiceNumber = invoiceNumber; + _status = PaymentStatus.waitingForPayment; + }); + _startPolling(invoiceNumber); + } else if (mounted) { + setState(() => _status = PaymentStatus.paymentError); + } + } + + void _startPolling(String invoiceNumber) { + const timeoutDuration = Duration(minutes: 15); // زيادة المهلة + var elapsed = Duration.zero; + + _pollingTimer = Timer.periodic(const Duration(seconds: 5), (timer) async { + elapsed += const Duration(seconds: 5); + if (elapsed >= timeoutDuration) { + timer.cancel(); + if (mounted) setState(() => _status = PaymentStatus.paymentTimeout); + return; + } + + debugPrint("Polling... Checking MTN invoice: $invoiceNumber"); + final isCompleted = + await _paymentService.checkInvoiceStatus(invoiceNumber); + if (isCompleted && mounted) { + timer.cancel(); + setState(() => _status = PaymentStatus.paymentSuccess); + } + }); + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: _status != PaymentStatus.waitingForPayment, + onPopInvoked: (didPop) async { + if (didPop) return; + if (_status == PaymentStatus.waitingForPayment) { + final shouldPop = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('هل أنت متأكد؟'), + content: const Text( + 'إذا خرجت الآن، قد تفشل عملية الدفع. عليك إتمامها من تطبيق MTN.'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('البقاء')), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text('الخروج')), + ], + ), + ); + if (shouldPop ?? false) { + Navigator.of(context).pop(); + } + } + }, + child: Scaffold( + appBar: AppBar(title: const Text("الدفع عبر MTN Cash")), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Center(child: _buildContentByStatus()), + ), + ), + ); + } + + Widget _buildContentByStatus() { + switch (_status) { + case PaymentStatus.creatingInvoice: + return const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 20), + Text("جاري إنشاء فاتورة دفع...", style: TextStyle(fontSize: 16)), + ], + ); + case PaymentStatus.waitingForPayment: + return _buildWaitingForPaymentUI(); + case PaymentStatus.paymentSuccess: + return _buildSuccessUI(); + case PaymentStatus.paymentTimeout: + case PaymentStatus.paymentError: + return _buildErrorUI(); + } + } + + Widget _buildWaitingForPaymentUI() { + final currencyFormat = NumberFormat.decimalPattern('ar_SY'); + + return SingleChildScrollView( + child: Column( + children: [ + // **مهم**: استبدل هذا المسار بمسار شعار MTN الصحيح في مشروعك + Image.asset('assets/images/cashMTN.png', width: 120), + const SizedBox(height: 24), + Text("تعليمات الدفع", style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 12), + Text( + "المبلغ المطلوب: ${currencyFormat.format(widget.amount)} ل.س", + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + Card( + elevation: 1.5, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: const Padding( + padding: EdgeInsets.all(16), + child: Column( + children: [ + _StepTile(number: 1, text: "افتح تطبيق MTN Cash Mobile."), + _StepTile( + number: 2, + text: "اذهب إلى قسم 'دفع الفواتير' أو 'خدمات الدفع'."), + _StepTile( + number: 3, + text: "ابحث عن 'Intaleq App' في قائمة المفوترين."), + _StepTile( + number: 4, + text: + "أدخل رقم هاتفك المسجل لدينا للاستعلام عن الفاتورة."), + _StepTile( + number: 5, + text: + "ستظهر لك فاتورة بالمبلغ المطلوب. قم بتأكيد الدفع."), + ], + ), + ), + ), + const SizedBox(height: 24), + const LinearProgressIndicator(minHeight: 2), + const SizedBox(height: 12), + Text("بانتظار تأكيد الدفع من MTN...", + style: TextStyle(color: Colors.grey.shade700)), + const SizedBox(height: 4), + const Text("هذه الشاشة ستتحدث تلقائيًا عند اكتمال الدفع", + style: TextStyle(color: Colors.grey), + textAlign: TextAlign.center), + ], + ), + ); + } + + Widget _buildSuccessUI() { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.check_circle, color: Colors.green, size: 80), + const SizedBox(height: 20), + const Text("تم الدفع بنجاح!", + style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + const Text("تمت إضافة النقاط إلى حسابك.", + style: TextStyle(fontSize: 16)), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text("العودة إلى المحفظة"), + ), + ], + ); + } + + Widget _buildErrorUI() { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error, color: Colors.red, size: 80), + const SizedBox(height: 20), + Text( + _status == PaymentStatus.paymentTimeout + ? "انتهى الوقت المحدد للدفع" + : "حدث خطأ أثناء إنشاء الفاتورة", + style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + const Text("يرجى المحاولة مرة أخرى.", style: TextStyle(fontSize: 16)), + const SizedBox(height: 20), + ElevatedButton( + onPressed: _createAndPollInvoice, + child: const Text("المحاولة مرة أخرى"), + ), + ], + ); + } +} + +// ويدجت مساعد لعرض خطوات التعليمات بشكل أنيق +class _StepTile extends StatelessWidget { + final int number; + final String text; + const _StepTile({required this.number, required this.text}); + + @override + Widget build(BuildContext context) { + return ListTile( + contentPadding: const EdgeInsets.symmetric(vertical: 4.0), + leading: CircleAvatar( + radius: 14, + backgroundColor: Theme.of(context).primaryColor, + child: Text("$number", + style: const TextStyle( + fontSize: 12, + color: Colors.white, + fontWeight: FontWeight.bold)), + ), + title: Text(text), + ); + } +} diff --git a/lib/controller/payment/smsPaymnet/payment_services.dart b/lib/controller/payment/smsPaymnet/payment_services.dart index 1b37802..62b1b28 100644 --- a/lib/controller/payment/smsPaymnet/payment_services.dart +++ b/lib/controller/payment/smsPaymnet/payment_services.dart @@ -16,7 +16,7 @@ import '../../../main.dart'; /// خدمة لإدارة عمليات الدفع المتعلقة بنظام الدفع عبر الرسائل القصيرة class PaymentService { - final String _baseUrl = "${AppLink.seferPaymentServer}/sms_webhook"; + final String _baseUrl = "${AppLink.paymentServer}/sms_webhook"; Future createInvoice({ required String userPhone, diff --git a/lib/main.dart b/lib/main.dart index 743c123..5dfb9df 100755 --- a/lib/main.dart +++ b/lib/main.dart @@ -227,7 +227,7 @@ class _MyAppState extends State with WidgetsBindingObserver { } if (!Get.isRegistered()) { - Get.put(FirebaseMessagesController()); + Get.put(FirebaseMessagesController()).getToken(); } await FirebaseMessaging.instance.requestPermission(); diff --git a/lib/views/auth/captin/otp_page.dart b/lib/views/auth/captin/otp_page.dart index ab493a3..723144c 100644 --- a/lib/views/auth/captin/otp_page.dart +++ b/lib/views/auth/captin/otp_page.dart @@ -15,16 +15,6 @@ import '../../../print.dart'; // Assuming you have an AppColor class defined in your project. // import 'path/to/your/app_color.dart'; -// --- Placeholder AppColor Class --- -// This is used to make the code runnable. -// You should use the one from your project. -// class AppColor { -// static const Color primaryColor = Color(0xFF1DA1F2); -// static const Color greenColor = Color(0xFF34A853); // Google Green -// static const Color secondaryColor = Colors.white; -// } -// --- End of Placeholder --- - /// A visually revamped authentication screen with a light, glassy effect, /// themed for the driver application using a green primary color. class AuthScreen extends StatelessWidget { diff --git a/lib/views/home/Captin/driver_map_page.dart b/lib/views/home/Captin/driver_map_page.dart index af058df..951d7b1 100755 --- a/lib/views/home/Captin/driver_map_page.dart +++ b/lib/views/home/Captin/driver_map_page.dart @@ -47,7 +47,7 @@ class PassengerLocationMapPage extends StatelessWidget { // 4. نافذة معلومات الراكب في الأسفل (تظهر قبل بدء الرحلة) - const PassengerInfoWindow(), + PassengerInfoWindow(), // 3. زر إلغاء الرحلة في الأعلى يسارًا CancelWidget(mapDriverController: mapDriverController), @@ -56,7 +56,7 @@ class PassengerLocationMapPage extends StatelessWidget { driverEndRideBar(), // 6. أزرار الطوارئ والاتصال - const SosConnect(), + SosConnect(), // 7. دائرة عرض السرعة speedCircle(), diff --git a/lib/views/home/Captin/home_captain/home_captin.dart b/lib/views/home/Captin/home_captain/home_captin.dart index b52e648..6ad8e16 100755 --- a/lib/views/home/Captin/home_captain/home_captin.dart +++ b/lib/views/home/Captin/home_captain/home_captin.dart @@ -1,5 +1,7 @@ +import 'dart:io'; import 'dart:ui'; +import 'package:bubble_head/bubble.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -8,6 +10,7 @@ import 'package:flutter_font_icons/flutter_font_icons.dart'; import 'package:sefer_driver/views/home/Captin/home_captain/drawer_captain.dart'; import 'package:sefer_driver/views/widgets/mycircular.dart'; +import '../../../../constant/box_name.dart'; import '../../../../constant/colors.dart'; import '../../../../constant/info.dart'; import '../../../../constant/style.dart'; @@ -15,7 +18,11 @@ import '../../../../controller/functions/location_controller.dart'; import '../../../../controller/functions/overlay_permisssion.dart'; import '../../../../controller/functions/package_info.dart'; import '../../../../controller/home/captin/home_captain_controller.dart'; +import '../../../../controller/home/captin/map_driver_controller.dart'; +import '../../../../main.dart'; +import '../../../notification/available_rides_page.dart'; import '../../../widgets/circle_container.dart'; +import '../driver_map_page.dart'; import 'widget/connect.dart'; import 'widget/left_menu_map_captain.dart'; @@ -51,7 +58,7 @@ class HomeCaptain extends StatelessWidget { // 2. The new floating "Status Pod" at the bottom. const _StatusPodOverlay(), - + FloatingActionButtons(), // This widget from the original code remains. leftMainMenuCaptainIcons(), ], @@ -465,44 +472,112 @@ class _MapControlButton extends StatelessWidget { } } -/// NOTE: The _FloatingActionButtons and _MapControlButton widgets have been removed -/// as their functionality is now integrated into the _HomeAppBar. -/// -/// You will still need to modify your existing `ConnectWidget` -/// to accept an `isCompact` boolean flag as mentioned in the previous design. -/* -class ConnectWidget extends StatelessWidget { - final bool isCompact; - const ConnectWidget({super.key, this.isCompact = false}); +class FloatingActionButtons extends StatelessWidget { + const FloatingActionButtons(); @override Widget build(BuildContext context) { - // ... your existing controller logic - - if (isCompact) { - // Return a smaller version for the pod - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: controller.isConnect ? AppColor.greenColor : AppColor.accentColor, - borderRadius: BorderRadius.circular(16), - ), - child: Row( - mainAxisSize: MainAxisSize.min, + // نفس الكود الأصلي للأزرار + return Positioned( + bottom: Get.height * .2, + right: 6, + child: + GetBuilder(builder: (homeCaptainController) { + return Column( children: [ - Icon(controller.isConnect ? Icons.wifi_tethering_rounded : Icons.wifi_tethering_off_rounded, color: Colors.white, size: 20), - const SizedBox(width: 8), - Text( - controller.isConnect ? 'Online'.tr : 'Offline'.tr, - style: AppStyle.title.copyWith(color: Colors.white, fontSize: 14), + Platform.isAndroid + ? AnimatedContainer( + duration: const Duration(microseconds: 200), + width: homeCaptainController.widthMapTypeAndTraffic, + decoration: BoxDecoration( + border: Border.all(color: AppColor.blueColor), + color: AppColor.secondaryColor, + borderRadius: BorderRadius.circular(15)), + child: IconButton( + onPressed: () async { + Bubble().startBubbleHead(sendAppToBackground: true); + }, + icon: Image.asset( + 'assets/images/logo.png', + fit: BoxFit.cover, + width: 35, + height: 35, + ), + ), + ) + : const SizedBox(), + const SizedBox( + height: 5, ), + AnimatedContainer( + duration: const Duration(microseconds: 200), + width: homeCaptainController.widthMapTypeAndTraffic, + decoration: BoxDecoration( + border: Border.all(color: AppColor.blueColor), + color: AppColor.secondaryColor, + borderRadius: BorderRadius.circular(15)), + child: IconButton( + onPressed: () { + Get.to(() => const AvailableRidesPage()); + }, + icon: const Icon( + Icons.train_sharp, + size: 29, + color: AppColor.blueColor, + ), + ), + ), + const SizedBox( + height: 5, + ), + box.read(BoxName.rideStatus) == 'Applied' || + box.read(BoxName.rideStatus) == 'Begin' + ? Positioned( + bottom: Get.height * .2, + right: 6, + child: AnimatedContainer( + duration: const Duration(microseconds: 200), + width: homeCaptainController.widthMapTypeAndTraffic, + decoration: BoxDecoration( + border: Border.all(color: AppColor.blueColor), + color: AppColor.secondaryColor, + borderRadius: BorderRadius.circular(15)), + child: GestureDetector( + onLongPress: () { + box.write(BoxName.rideStatus, 'delete'); + homeCaptainController.update(); + }, + child: IconButton( + onPressed: () { + box.read(BoxName.rideStatus) == 'Applied' + ? { + Get.to(() => PassengerLocationMapPage(), + arguments: + box.read(BoxName.rideArguments)), + Get.put(MapDriverController()) + .changeRideToBeginToPassenger() + } + : { + Get.to(() => PassengerLocationMapPage(), + arguments: + box.read(BoxName.rideArguments)), + Get.put(MapDriverController()) + .startRideFromStartApp() + }; + }, + icon: const Icon( + Icons.directions_rounded, + size: 29, + color: AppColor.blueColor, + ), + ), + ), + ), + ) + : const SizedBox() ], - ), - ); - } - - // Return the original, larger button - return ElevatedButton.icon(...) + ); + }), + ); } } -*/ diff --git a/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart b/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart index 3f99b5c..9f43432 100755 --- a/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart +++ b/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart @@ -1,9 +1,8 @@ +import 'package:flutter_overlay_window/flutter_overlay_window.dart'; import 'package:sefer_driver/constant/box_name.dart'; import 'package:sefer_driver/controller/firebase/local_notification.dart'; -import 'package:sefer_driver/controller/functions/network/net_guard.dart'; -import 'package:sefer_driver/controller/functions/sms_egypt_controller.dart'; import 'package:sefer_driver/main.dart'; -import 'package:sefer_driver/views/auth/captin/login_captin.dart'; +import 'package:sefer_driver/views/auth/captin/otp_page.dart'; import 'package:sefer_driver/views/home/Captin/driver_map_page.dart'; import 'package:sefer_driver/views/home/Captin/orderCaptin/vip_order_page.dart'; import 'package:flutter/material.dart'; @@ -16,13 +15,10 @@ import '../../../../../constant/colors.dart'; import '../../../../../constant/links.dart'; import '../../../../../controller/firebase/firbase_messge.dart'; import '../../../../../controller/functions/crud.dart'; -import '../../../../../controller/functions/encrypt_decrypt.dart'; import '../../../../../controller/home/captin/order_request_controller.dart'; import '../../../../../controller/home/navigation/navigation_view.dart'; +import '../../../../../print.dart'; import '../../../../Rate/ride_calculate_driver.dart'; -import '../../../../auth/captin/otp_page.dart'; -import '../../../../auth/syria/registration_view.dart'; -import '../../../../widgets/error_snakbar.dart'; GetBuilder leftMainMenuCaptainIcons() { final firebaseMessagesController = @@ -186,7 +182,8 @@ GetBuilder leftMainMenuCaptainIcons() { // child: Builder(builder: (context) { // return IconButton( // onPressed: () async { - // Get.to(PhoneNumberScreen()); + // Get.to(() => const PhoneNumberScreen()); + // // box.write(BoxName.statusDriverLocation, 'off'); // }, // icon: const Icon( // FontAwesome5.grin_tears, diff --git a/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart b/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart index 037fa39..7fe1faf 100755 --- a/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart +++ b/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart @@ -13,8 +13,10 @@ import '../../../../main.dart'; // Changed: إعادة تصميم كاملة لتصبح شريط معلومات علوي مدمج class PassengerInfoWindow extends StatelessWidget { - const PassengerInfoWindow({super.key}); - + PassengerInfoWindow({super.key}); + final fcm = Get.isRegistered() + ? Get.find() + : Get.put(FirebaseMessagesController()); @override Widget build(BuildContext context) { return GetBuilder( @@ -152,8 +154,7 @@ class PassengerInfoWindow extends StatelessWidget { if (await controller .calculateDistanceBetweenDriverAndPassengerLocation() < 140) { - Get.find() - .sendNotificationToDriverMAP( + fcm.sendNotificationToDriverMAP( 'Hi ,I Arrive your site', 'I Arrive at your site'.tr, controller.tokenPassenger, @@ -238,7 +239,7 @@ class PassengerInfoWindow extends StatelessWidget { kolor: AppColor.deepPurpleAccent, onPressed: () { MyDialog().getDialog('Are you sure to cancel?'.tr, '', () async { - Get.find().sendNotificationToDriverMAP( + fcm.sendNotificationToDriverMAP( 'Driver Cancelled Your Trip', 'You will need to pay the cost to the driver, or it will be deducted from your next trip' .tr, diff --git a/lib/views/home/Captin/mapDriverWidgets/sos_connect.dart b/lib/views/home/Captin/mapDriverWidgets/sos_connect.dart index 8b3269e..aa3188d 100755 --- a/lib/views/home/Captin/mapDriverWidgets/sos_connect.dart +++ b/lib/views/home/Captin/mapDriverWidgets/sos_connect.dart @@ -207,8 +207,10 @@ import '../../../../main.dart'; // Changed: إعادة تصميم وتغيير موضع أزرار التواصل والطوارئ class SosConnect extends StatelessWidget { - const SosConnect({super.key}); - + SosConnect({super.key}); + final fcm = Get.isRegistered() + ? Get.find() + : Get.put(FirebaseMessagesController()); @override Widget build(BuildContext context) { return GetBuilder( @@ -337,25 +339,23 @@ class SosConnect extends StatelessWidget { _buildMessageTile( text: "Where are you, sir?".tr, onTap: () { - Get.find() - .sendNotificationToDriverMAP( - 'message From Driver', - "Where are you, sir?".tr, - controller.tokenPassenger, - [], - 'ding.wav'); + fcm.sendNotificationToDriverMAP( + 'message From Driver', + "Where are you, sir?".tr, + controller.tokenPassenger, + [], + 'ding.wav'); Get.back(); }), _buildMessageTile( text: "I've been trying to reach you but your phone is off.".tr, onTap: () { - Get.find() - .sendNotificationToDriverMAP( - 'message From Driver', - "I've been trying to reach you but your phone is off.".tr, - controller.tokenPassenger, - [], - 'ding.wav'); + fcm.sendNotificationToDriverMAP( + 'message From Driver', + "I've been trying to reach you but your phone is off.".tr, + controller.tokenPassenger, + [], + 'ding.wav'); Get.back(); }), const SizedBox(height: 16), @@ -374,13 +374,12 @@ class SosConnect extends StatelessWidget { ), IconButton( onPressed: () { - Get.find() - .sendNotificationToDriverMAP( - 'message From Driver', - controller.messageToPassenger.text, - controller.tokenPassenger, - [], - 'ding.wav'); + fcm.sendNotificationToDriverMAP( + 'message From Driver', + controller.messageToPassenger.text, + controller.tokenPassenger, + [], + 'ding.wav'); controller.messageToPassenger.clear(); Get.back(); }, diff --git a/lib/views/home/my_wallet/points_captain.dart b/lib/views/home/my_wallet/points_captain.dart index c66ec94..d26bdee 100755 --- a/lib/views/home/my_wallet/points_captain.dart +++ b/lib/views/home/my_wallet/points_captain.dart @@ -13,6 +13,7 @@ import 'package:webview_flutter/webview_flutter.dart'; import '../../../constant/box_name.dart'; import '../../../constant/links.dart'; import '../../../controller/functions/crud.dart'; +import '../../../controller/payment/mtn_new/mtn_payment_new_screen.dart'; import '../../../main.dart'; import '../../../print.dart'; import '../../widgets/elevated_btn.dart'; @@ -65,48 +66,49 @@ class PointsCaptain extends StatelessWidget { color: AppColor.blueColor, size: 70), ], )), - GestureDetector( - onTap: () async { - Get.back(); - Get.defaultDialog( - barrierDismissible: false, - title: 'Insert Wallet phone number'.tr, - content: Form( - key: paymentController.formKey, - child: MyTextForm( - controller: - paymentController.walletphoneController, - label: 'Insert Wallet phone number'.tr, - hint: '963941234567', - type: TextInputType.phone)), - confirm: MyElevatedButton( - title: 'OK'.tr, - onPressed: () async { - Get.back(); - if (paymentController.formKey.currentState! - .validate()) { - box.write( - BoxName.phoneWallet, - paymentController - .walletphoneController.text); - await payWithMTNWallet( - context, pricePoint.toString(), 'SYP'); - } - })); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Pay by MTN Wallet'.tr), - const SizedBox(width: 10), - Image.asset( - 'assets/images/cashMTN.png', - width: 70, - height: 70, - fit: BoxFit.fill, - ), - ], - )), + // GestureDetector( + // onTap: () async { + // Get.back(); + // Get.defaultDialog( + // barrierDismissible: false, + // title: 'Insert Wallet phone number'.tr, + // content: Form( + // key: paymentController.formKey, + // child: MyTextForm( + // controller: + // paymentController.walletphoneController, + // label: 'Insert Wallet phone number'.tr, + // hint: '963941234567', + // type: TextInputType.phone)), + // confirm: MyElevatedButton( + // title: 'OK'.tr, + // onPressed: () async { + // Get.back(); + // if (paymentController.formKey.currentState! + // .validate()) { + // box.write( + // BoxName.phoneWallet, + // paymentController + // .walletphoneController.text); + // await payWithMTNWallet( + // context, pricePoint.toString(), 'SYP'); + // } + // })); + // }, + // child: Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Text('Pay by MTN Wallet'.tr), + // const SizedBox(width: 10), + // Image.asset( + // 'assets/images/cashMTN.png', + // width: 70, + // height: 70, + // fit: BoxFit.fill, + // ), + // ], + // )), + GestureDetector( onTap: () async { Get.back(); @@ -210,6 +212,69 @@ class PointsCaptain extends StatelessWidget { ), ], )), + GestureDetector( + onTap: () async { + Get.back(); + Get.defaultDialog( + barrierDismissible: false, + title: 'Insert Wallet phone number'.tr, + content: Form( + key: paymentController.formKey, + child: MyTextForm( + controller: + paymentController.walletphoneController, + label: 'Insert Wallet phone number'.tr, + hint: '963941234567', + type: TextInputType.phone)), + confirm: MyElevatedButton( + title: 'OK'.tr, + onPressed: () async { + Get.back(); + if (paymentController.formKey.currentState! + .validate()) { + box.write( + BoxName.phoneWallet, + paymentController + .walletphoneController.text); + // await payWithSyriaTelWallet( + // context, pricePoint.toString(), 'SYP'); + bool isAuthSupported = + await LocalAuthentication() + .isDeviceSupported(); + if (isAuthSupported) { + bool didAuthenticate = + await LocalAuthentication() + .authenticate( + localizedReason: + 'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع', + ); + if (!didAuthenticate) { + if (Get.isDialogOpen ?? false) Get.back(); + print( + "❌ User did not authenticate with biometrics"); + return; + } + } + Get.to(() => PaymentScreenMtn( + amount: pricePoint, + userType: 'Driver', + )); + } + })); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Pay by MTN Wallet'.tr), + const SizedBox(width: 10), + Image.asset( + 'assets/images/cashMTN.png', + width: 70, + height: 70, + fit: BoxFit.fill, + ), + ], + )), ], )); }, diff --git a/lib/views/notification/available_rides_page.dart b/lib/views/notification/available_rides_page.dart index 5c5dc4c..c3eb7da 100755 --- a/lib/views/notification/available_rides_page.dart +++ b/lib/views/notification/available_rides_page.dart @@ -1,104 +1,49 @@ -import 'dart:convert'; - -import 'package:sefer_driver/constant/colors.dart'; -import 'package:sefer_driver/constant/style.dart'; -import 'package:sefer_driver/controller/notification/ride_available_controller.dart'; -import 'package:sefer_driver/views/widgets/my_scafold.dart'; -import 'package:sefer_driver/views/widgets/mycircular.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'dart:math'; import '../../constant/box_name.dart'; +import '../../constant/colors.dart'; import '../../constant/links.dart'; +import '../../constant/style.dart'; import '../../controller/firebase/firbase_messge.dart'; import '../../controller/functions/crud.dart'; import '../../controller/home/captin/home_captain_controller.dart'; +import '../../controller/notification/ride_available_controller.dart'; import '../../main.dart'; import '../home/Captin/driver_map_page.dart'; +import '../widgets/my_scafold.dart'; +import '../widgets/mycircular.dart'; import '../widgets/mydialoug.dart'; +// --- Placeholder Classes and Variables (for demonstration) --- +// These are dummy implementations to make the code runnable. +// You should use your actual project files. + +// --- End of Placeholder Classes --- + class AvailableRidesPage extends StatelessWidget { const AvailableRidesPage({super.key}); @override Widget build(BuildContext context) { - Get.put(RideAvailableController()); + // Use findOrPut to avoid re-creating the controller on rebuilds + Get.lazyPut(() => RideAvailableController()); + Get.lazyPut(() => HomeCaptainController()); + return GetBuilder( builder: (rideAvailableController) { - // rideAvailableController.sortRidesByDistance(); + // rideAvailableController.sortRidesByDistance(); // Original logic return MyScafolld( title: 'Available for rides'.tr, body: [ rideAvailableController.isLoading ? const MyCircularProgressIndicator() - : - // : ListView.builder( - // itemCount: rideAvailableController - // .rideAvailableMap['message'] - // .where((rideInfo) { - // var driverType = - // box.read(BoxName.carTypeOfDriver).toString(); - // return (driverType == 'Comfort' && - // ['Speed', 'Comfort'] - // .contains(rideInfo['carType'])) || - // (driverType == 'Speed' && - // rideInfo['carType'] == 'Speed') || - // (driverType == 'Scooter' && - // rideInfo['carType'] == 'Scooter') || - // (driverType == 'Awfar Car' && - // rideInfo['carType'] == 'Awfar Car') || - // (driverType == 'Lady' && - // ['Comfort', 'Speed', 'Lady'] - // .contains(rideInfo['carType'])); - // }).length, - // itemBuilder: (context, index) { - // var filteredRides = rideAvailableController - // .rideAvailableMap['message'] - // .where((rideInfo) { - // var driverType = - // box.read(BoxName.carTypeOfDriver).toString(); - // return (driverType == 'Comfort' && - // ['Speed', 'Comfort'] - // .contains(rideInfo['carType'])) || - // (driverType == 'Speed' && - // rideInfo['carType'] == 'Speed') || - // (driverType == 'Awfar Car' && - // rideInfo['carType'] == 'Awfar Car') || - // (driverType == 'Scooter' && - // rideInfo['carType'] == 'Scooter') || - // (driverType == 'Lady' && - // ['Comfort', 'Speed', 'Lady'] - // .contains(rideInfo['carType'])); - // }).toList(); - - // return RideAvailableCard( - // rideInfo: filteredRides[index], - // ); - // }, - // ) - ListView.builder( - itemCount: rideAvailableController - .rideAvailableMap['message'] - .where((rideInfo) { - var driverType = - box.read(BoxName.carTypeOfDriver).toString(); - switch (driverType) { - case 'Comfort': - return ['Speed', 'Comfort'] - .contains(rideInfo['carType']); - case 'Speed': - case 'Scooter': - case 'Awfar Car': - return rideInfo['carType'] == driverType; - case 'Lady': - return ['Comfort', 'Speed', 'Lady'] - .contains(rideInfo['carType']); - default: - return false; - } - }).length, - itemBuilder: (context, index) { - var filteredRides = rideAvailableController + : Builder( + builder: (context) { + // Filtering logic remains the same + final filteredRides = rideAvailableController .rideAvailableMap['message'] .where((rideInfo) { var driverType = @@ -119,21 +64,27 @@ class AvailableRidesPage extends StatelessWidget { } }).toList(); - return RideAvailableCard( - rideInfo: filteredRides[index], + if (filteredRides.isEmpty) { + return Center( + child: Text( + "No rides available for your vehicle type.".tr, + style: AppStyle.subtitle, + ), + ); + } + + return ListView.builder( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 16), + itemCount: filteredRides.length, + itemBuilder: (context, index) { + return RideAvailableCard( + rideInfo: filteredRides[index], + ); + }, ); }, ) - // rideAvailableController.isLoading - // ? const MyCircularProgressIndicator() - // : ListView.builder( - // itemCount: rideAvailableController - // .rideAvailableMap['message'].length, - // itemBuilder: (context, index) => RideAvailableCard( - // rideInfo: rideAvailableController - // .rideAvailableMap['message'][index], - // ), - // ) ], isleading: true); }); @@ -147,90 +98,189 @@ class RideAvailableCard extends StatelessWidget { @override Widget build(BuildContext context) { + // The main card with improved styling return Card( - margin: const EdgeInsets.all(8.0), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - elevation: 4, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildLocationRow('↑', rideInfo['startName'], AppColor.greenColor), - const SizedBox(height: 8), - _buildLocationRow('↓', rideInfo['endName'], Colors.red), - const SizedBox(height: 16), - _buildInfoRow(), - const SizedBox(height: 16), - _buildActionRow(), - ], + margin: const EdgeInsets.only(bottom: 16.0), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + elevation: 5, + shadowColor: Colors.black.withOpacity(0.1), + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () { + // You can add an action here, e.g., show ride details on a map + }, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(), + const SizedBox(height: 16), + _buildRouteInfo(), + const Divider(height: 32), + _buildRideDetails(), + const SizedBox(height: 20), + _buildAcceptButton(), + ], + ), ), ), ); } - Widget _buildLocationRow(String icon, String location, Color iconColor) { - return Row( - children: [ - Text( - icon, - style: TextStyle( - fontSize: 20, fontWeight: FontWeight.bold, color: iconColor), - ), - const SizedBox(width: 8), - Expanded( - child: Text( - location, - style: AppStyle.subtitle, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ); - } - - Widget _buildInfoRow() { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('${'Price:'.tr} ${rideInfo['price']} \$', style: AppStyle.title), - Text( - rideInfo['carType'], - style: AppStyle.title.copyWith(color: AppColor.greenColor), - ), - ], - ); - } - - Widget _buildActionRow() { + // Header section with Price and Car Type + Widget _buildHeader() { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('📈 ${rideInfo['passengerRate']}', style: AppStyle.title), + Text('Fare'.tr, style: AppStyle.subtitle.copyWith(fontSize: 12)), const SizedBox(height: 4), - Text( - '📍 ${rideInfo['distance']} ${'KM'.tr}', - style: AppStyle.title.copyWith(color: AppColor.greenColor), - ), + Text('${rideInfo['price']} \$', + style: AppStyle.title + .copyWith(fontSize: 24, color: AppColor.primaryColor)), ], ), - ElevatedButton( - onPressed: () => _acceptRide(), - style: ElevatedButton.styleFrom( - backgroundColor: AppColor.greenColor, - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: AppColor.greenColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + rideInfo['carType'], + style: AppStyle.title + .copyWith(color: AppColor.greenColor, fontSize: 12), ), - child: Text('Accept'.tr), ), ], ); } + // Visual representation of the pickup and dropoff route + Widget _buildRouteInfo() { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Dotted line and icons column + Column( + children: [ + const Icon(CupertinoIcons.circle_fill, + color: AppColor.greenColor, size: 20), + ...List.generate( + 4, + (index) => Container( + height: 4, + width: 2, + color: AppColor.writeColor, + margin: const EdgeInsets.symmetric(vertical: 2), + )), + const Icon(CupertinoIcons.location_solid, + color: Colors.red, size: 20), + ], + ), + const SizedBox(width: 16), + // Location text column + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildLocationText(rideInfo['startName'], 'Pickup'.tr), + const SizedBox(height: 20), + _buildLocationText(rideInfo['endName'], 'Dropoff'.tr), + ], + ), + ) + ], + ); + } + + // Helper for location text + Widget _buildLocationText(String location, String label) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(label, style: AppStyle.subtitle.copyWith(fontSize: 12)), + const SizedBox(height: 2), + Text( + location, + style: AppStyle.title.copyWith(fontWeight: FontWeight.normal), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ); + } + + // Ride details section with Distance and Passenger Rating + Widget _buildRideDetails() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildInfoChip( + icon: CupertinoIcons.map_pin_ellipse, + value: '${rideInfo['distance']} ${'KM'.tr}', + label: 'Distance'.tr, + color: AppColor.primaryColor, + ), + _buildInfoChip( + icon: CupertinoIcons.star_fill, + value: '${rideInfo['passengerRate']}', + label: 'Rating'.tr, + color: Colors.amber, + ), + ], + ); + } + + // A reusable chip for displaying info with an icon + Widget _buildInfoChip( + {required IconData icon, + required String value, + required String label, + required Color color}) { + return Column( + children: [ + Row( + children: [ + Icon(icon, color: color, size: 16), + const SizedBox(width: 8), + Text(value, style: AppStyle.title), + ], + ), + const SizedBox(height: 4), + Text(label, style: AppStyle.subtitle.copyWith(fontSize: 12)), + ], + ); + } + + // The accept button with improved styling + Widget _buildAcceptButton() { + return SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + icon: const Icon(Icons.check_circle_outline, color: Colors.white), + label: Text('Accept'.tr, + style: const TextStyle( + color: Colors.white, fontWeight: FontWeight.bold)), + onPressed: _acceptRide, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.greenColor, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + elevation: 2, + ), + ), + ); + } + + // --- Ride Acceptance Logic --- + // This logic is copied exactly from your original code. void _acceptRide() async { var res = await CRUD().post(link: AppLink.updateStausFromSpeed, payload: { 'id': rideInfo['id'], @@ -249,8 +299,6 @@ class RideAvailableCard extends StatelessWidget { }); } - // .then((value) { - // var json = jsonDecode(res); if (res != "failure") { List bodyToPassenger = [ box.read(BoxName.driverID).toString(), @@ -276,7 +324,6 @@ class RideAvailableCard extends StatelessWidget { link: '${AppLink.endPoint}/driver_order/add.php', payload: { 'driver_id': box.read(BoxName.driverID), - // box.read(BoxName.driverID).toString(), 'order_id': rideInfo['id'], 'status': 'Apply' }); @@ -294,9 +341,7 @@ class RideAvailableCard extends StatelessWidget { FirebaseMessagesController().sendNotificationToPassengerToken( "Accepted Ride".tr, 'your ride is Accepted'.tr, - // arguments['DriverList'][9].toString(), rideInfo['passengerToken'].toString(), - // box.read(BoxName.tokenDriver).toString(), bodyToPassenger, 'start.wav'); Get.back(); @@ -319,10 +364,7 @@ class RideAvailableCard extends StatelessWidget { 'driverId': box.read(BoxName.driverID).toString(), 'durationOfRideValue': rideInfo['duration'].toString(), 'paymentAmount': rideInfo['price'].toString(), - 'paymentMethod': 'cash'.toString() == //todo fix payment method - 'true' - ? 'visa' - : 'cash', + 'paymentMethod': 'cash'.toString() == 'true' ? 'visa' : 'cash', 'isHaveSteps': 'startEnd'.toString(), 'step0': ''.toString(), 'step1': ''.toString(),