25-10-5/1
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user