25-10-5/1
This commit is contained in:
@@ -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 =
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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.');
|
||||
|
||||
@@ -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<dynamic> 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<MapPassengerController>().driverToken, []);
|
||||
// Get.find<MapPassengerController>()
|
||||
// .startTimerDriverWaitPassenger5Minute();
|
||||
|
||||
Get.back();
|
||||
}));
|
||||
}
|
||||
|
||||
late String serviceAccountKeyJson;
|
||||
@override
|
||||
Future<void> 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<String> all =
|
||||
List<String>.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: <String, String>{
|
||||
'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,
|
||||
},
|
||||
|
||||
71
lib/controller/firebase/notification_service.dart
Normal file
71
lib/controller/firebase/notification_service.dart
Normal file
@@ -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<void> 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
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -335,7 +335,7 @@ class MapDriverController extends GetxController {
|
||||
// Get.find<HomeCaptainController>().changeToAppliedRide('Applied');
|
||||
|
||||
Get.find<FirebaseMessagesController>().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<FirebaseMessagesController>()
|
||||
: 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<FirebaseMessagesController>().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<FirebaseMessagesController>().sendNotificationToDriverMAP(
|
||||
"You are near the destination".tr,
|
||||
"You are near the destination",
|
||||
"You are near the destination".tr,
|
||||
tokenPassenger,
|
||||
[
|
||||
|
||||
@@ -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<void> 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<void> initializeOrderPage() async {
|
||||
final myListString = Get.arguments['myListString'];
|
||||
Log.print('myListString0000: ${myListString}');
|
||||
|
||||
if (Get.arguments['DriverList'] == null ||
|
||||
Get.arguments['DriverList'].isEmpty) {
|
||||
|
||||
41
lib/controller/home/navigation/decode_polyline_isolate.dart
Normal file
41
lib/controller/home/navigation/decode_polyline_isolate.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
|
||||
// تم تعديل الدالة لتقبل وسيط من نوع `dynamic` لحل مشكلة عدم تطابق الأنواع مع دالة `compute`.
|
||||
// هذه الدالة لا تزال تعمل كدالة من المستوى الأعلى (Top-level function)
|
||||
// وهو شرط أساسي لاستخدامها مع دالة compute.
|
||||
List<LatLng> decodePolylineIsolate(dynamic encodedMessage) {
|
||||
// التأكد من أن الرسالة المستقبلة هي من نوع String
|
||||
if (encodedMessage is! String) {
|
||||
// إرجاع قائمة فارغة أو إظهار خطأ إذا كان النوع غير صحيح
|
||||
return [];
|
||||
}
|
||||
final String encoded = encodedMessage;
|
||||
|
||||
List<LatLng> 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;
|
||||
}
|
||||
@@ -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<void> _prepareStepData() async {
|
||||
_stepBounds.clear();
|
||||
_stepPolylines.clear();
|
||||
if (routeSteps.isEmpty) return;
|
||||
for (final step in routeSteps) {
|
||||
final pointsString = step['polyline']['points'];
|
||||
final List<List<num>> points =
|
||||
decodePolyline(pointsString).cast<List<num>>();
|
||||
final polylineCoordinates = points
|
||||
.map((point) => LatLng(point[0].toDouble(), point[1].toDouble()))
|
||||
.toList();
|
||||
// <<<--- التعديل الثاني: استخدام compute لفك التشفير في خيط منفصل
|
||||
// وتصحيح طريقة التعامل مع القائمة المُرجعة
|
||||
final List<LatLng> polylineCoordinates = await compute(
|
||||
decodePolylineIsolate as ComputeCallback<dynamic, List<LatLng>>,
|
||||
pointsString);
|
||||
|
||||
_stepPolylines.add(polylineCoordinates); // تخزين نقاط الخطوة
|
||||
_stepBounds.add(_boundsFromLatLngList(polylineCoordinates));
|
||||
}
|
||||
}
|
||||
|
||||
// ... باقي دوال الكنترولر بدون تغيير ...
|
||||
// (selectDestination, onMapLongPressed, startNavigationTo, getRoute, etc.)
|
||||
Future<void> selectDestination(dynamic place) async {
|
||||
placeDestinationController.clear();
|
||||
placesDestination = [];
|
||||
@@ -401,11 +401,11 @@ class NavigationController extends GetxController {
|
||||
}
|
||||
|
||||
Future<void> 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<List<num>> points =
|
||||
decodePolyline(pointsString).cast<List<num>>();
|
||||
_fullRouteCoordinates = points
|
||||
.map((point) => LatLng(point[0].toDouble(), point[1].toDouble()))
|
||||
.toList();
|
||||
|
||||
// <<<--- التعديل الثالث: استخدام compute هنا أيضًا للمسار الرئيسي
|
||||
_fullRouteCoordinates = await compute(
|
||||
decodePolylineIsolate as ComputeCallback<dynamic, List<LatLng>>,
|
||||
pointsString);
|
||||
|
||||
polylines.add(
|
||||
Polyline(
|
||||
@@ -441,7 +441,9 @@ class NavigationController extends GetxController {
|
||||
|
||||
routeSteps = List<Map<String, dynamic>>.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<void> 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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?": "ما هي مميزات محفظتنا؟",
|
||||
|
||||
@@ -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<LocationController>().myLocation.latitude;
|
||||
double currentLongitude =
|
||||
Get.find<LocationController>().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<void> getRideAvailable() async {
|
||||
try {
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
LatLngBounds bounds = calculateBounds(
|
||||
Get.find<LocationController>().myLocation!.latitude,
|
||||
Get.find<LocationController>().myLocation!.longitude,
|
||||
Get.find<LocationController>().myLocation.latitude,
|
||||
Get.find<LocationController>().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();
|
||||
}
|
||||
}
|
||||
|
||||
350
lib/controller/payment/mtn_new/mtn_payment_new_screen.dart
Normal file
350
lib/controller/payment/mtn_new/mtn_payment_new_screen.dart
Normal file
@@ -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<String?> 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<bool> 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<PaymentScreenMtn> {
|
||||
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<bool>(
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import '../../../main.dart';
|
||||
|
||||
/// خدمة لإدارة عمليات الدفع المتعلقة بنظام الدفع عبر الرسائل القصيرة
|
||||
class PaymentService {
|
||||
final String _baseUrl = "${AppLink.seferPaymentServer}/sms_webhook";
|
||||
final String _baseUrl = "${AppLink.paymentServer}/sms_webhook";
|
||||
|
||||
Future<String?> createInvoice({
|
||||
required String userPhone,
|
||||
|
||||
@@ -227,7 +227,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
if (!Get.isRegistered<FirebaseMessagesController>()) {
|
||||
Get.put(FirebaseMessagesController());
|
||||
Get.put(FirebaseMessagesController()).getToken();
|
||||
}
|
||||
|
||||
await FirebaseMessaging.instance.requestPermission();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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<HomeCaptainController>(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(...)
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -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<HomeCaptainController> leftMainMenuCaptainIcons() {
|
||||
final firebaseMessagesController =
|
||||
@@ -186,7 +182,8 @@ GetBuilder<HomeCaptainController> 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,
|
||||
|
||||
@@ -13,8 +13,10 @@ import '../../../../main.dart';
|
||||
|
||||
// Changed: إعادة تصميم كاملة لتصبح شريط معلومات علوي مدمج
|
||||
class PassengerInfoWindow extends StatelessWidget {
|
||||
const PassengerInfoWindow({super.key});
|
||||
|
||||
PassengerInfoWindow({super.key});
|
||||
final fcm = Get.isRegistered<FirebaseMessagesController>()
|
||||
? Get.find<FirebaseMessagesController>()
|
||||
: Get.put(FirebaseMessagesController());
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapDriverController>(
|
||||
@@ -152,8 +154,7 @@ class PassengerInfoWindow extends StatelessWidget {
|
||||
if (await controller
|
||||
.calculateDistanceBetweenDriverAndPassengerLocation() <
|
||||
140) {
|
||||
Get.find<FirebaseMessagesController>()
|
||||
.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<FirebaseMessagesController>().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,
|
||||
|
||||
@@ -207,8 +207,10 @@ import '../../../../main.dart';
|
||||
|
||||
// Changed: إعادة تصميم وتغيير موضع أزرار التواصل والطوارئ
|
||||
class SosConnect extends StatelessWidget {
|
||||
const SosConnect({super.key});
|
||||
|
||||
SosConnect({super.key});
|
||||
final fcm = Get.isRegistered<FirebaseMessagesController>()
|
||||
? Get.find<FirebaseMessagesController>()
|
||||
: Get.put(FirebaseMessagesController());
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapDriverController>(
|
||||
@@ -337,25 +339,23 @@ class SosConnect extends StatelessWidget {
|
||||
_buildMessageTile(
|
||||
text: "Where are you, sir?".tr,
|
||||
onTap: () {
|
||||
Get.find<FirebaseMessagesController>()
|
||||
.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<FirebaseMessagesController>()
|
||||
.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<FirebaseMessagesController>()
|
||||
.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();
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
));
|
||||
},
|
||||
|
||||
@@ -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<RideAvailableController>(
|
||||
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<String> 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(),
|
||||
|
||||
Reference in New Issue
Block a user