25-10-5/1

This commit is contained in:
Hamza-Ayed
2025-10-05 14:57:32 +03:00
parent 95fb065bdb
commit 1cc66029a3
28 changed files with 1347 additions and 666 deletions

View File

@@ -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 =

View File

@@ -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();
}
}
}

View File

@@ -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(),

View File

@@ -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.');

View File

@@ -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,
},

View 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
);
}

View File

@@ -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);

View File

@@ -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');

View File

@@ -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,
[

View File

@@ -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) {

View 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;
}

View File

@@ -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);
});
// خذ أول 1015 للعرض (اختياري)، أو اعرض الكل
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)));
}
}

View File

@@ -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?": "ما هي مميزات محفظتنا؟",

View File

@@ -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();
}
}

View 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),
);
}
}

View File

@@ -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,

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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(),

View File

@@ -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(...)
);
}),
);
}
}
*/

View File

@@ -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,

View File

@@ -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,

View File

@@ -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();
},

View File

@@ -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,
),
],
)),
],
));
},

View File

@@ -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(),