This commit is contained in:
Hamza-Ayed
2025-09-08 00:19:57 +03:00
parent 8c7f3e3a75
commit 7e904ae460
146 changed files with 761 additions and 327 deletions

View File

@@ -38,6 +38,7 @@ class AppLink {
static String ride = '$server/ride';
//===============contact==========================
static String savePhones = "$ride/egyptPhones/add.php";
static String savePhonesSyria = "$ride/egyptPhones/syrianAdd.php";
static String getPhones = "$ride/egyptPhones/get.php";
////===============firebase==========================

View File

@@ -1,21 +1,20 @@
import 'dart:convert';
import 'package:local_auth/local_auth.dart';
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/constant/links.dart';
import 'package:sefer_driver/controller/firebase/local_notification.dart';
import 'package:sefer_driver/controller/functions/crud.dart';
import 'package:sefer_driver/controller/functions/encrypt_decrypt.dart';
import 'package:sefer_driver/controller/home/payment/captain_wallet_controller.dart';
import 'package:sefer_driver/views/widgets/mydialoug.dart';
import 'package:flutter/material.dart';
import 'package:flutter_contacts/contact.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:get/get.dart';
import 'package:local_auth/local_auth.dart';
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/constant/links.dart';
import 'package:sefer_driver/controller/functions/crud.dart';
import 'package:sefer_driver/controller/home/payment/captain_wallet_controller.dart';
import 'package:sefer_driver/main.dart';
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
import 'package:sefer_driver/views/widgets/mydialoug.dart';
import 'package:share_plus/share_plus.dart';
import '../../../main.dart';
import '../../../views/widgets/error_snakbar.dart';
import '../../firebase/local_notification.dart';
import '../../functions/launch.dart';
import '../../notification/notification_captain_controller.dart';
@@ -26,6 +25,10 @@ class InviteController extends GetxController {
String? couponCode;
String? driverCouponCode;
// **FIX**: Added the missing 'contacts' and 'contactMaps' definitions.
List<Contact> contacts = [];
RxList<Map<String, dynamic>> contactMaps = <Map<String, dynamic>>[].obs;
int selectedTab = 0;
PassengerStats passengerStats = PassengerStats();
void updateSelectedTab(int index) {
@@ -33,14 +36,10 @@ class InviteController extends GetxController {
update();
}
Future<void> shareCouponCode() async {
// TODO: Implement sharing functionality
// You can use share_plus package to share the coupon code
}
Future<void> shareDriverCode() async {
if (driverCouponCode != null) {
final String shareText = '''
Join Intaleq as a driver using my referral code!
final String shareText =
'''Join Intaleq as a driver using my referral code!
Use code: $driverCouponCode
Download the Intaleq Driver app now and earn rewards!
''';
@@ -50,8 +49,7 @@ Download the Intaleq Driver app now and earn rewards!
Future<void> sharePassengerCode() async {
if (couponCode != null) {
final String shareText = '''
Get a discount on your first Intaleq ride!
final String shareText = '''Get a discount on your first Intaleq ride!
Use my referral code: $couponCode
Download the Intaleq app now and enjoy your ride!
''';
@@ -62,9 +60,98 @@ Download the Intaleq app now and enjoy your ride!
@override
void onInit() {
super.onInit();
// **MODIFIED**: Sync contacts automatically on controller initialization.
syncContactsToServerOnce();
// fetchDriverStats();
}
// --- NEW LOGIC: ONE-TIME CONTACTS SYNC ---
/// **NEW**: Syncs all phone contacts to the server, but only runs once per user.
Future<void> syncContactsToServerOnce() async {
final String syncFlagKey = 'contactsSynced_${box.read(BoxName.driverID)}';
// 1. Check if contacts have already been synced for this user.
if (box.read(syncFlagKey) == true) {
print("Contacts have already been synced for this user.");
return;
}
try {
// 2. Request permission and fetch all contacts.
if (await FlutterContacts.requestPermission(readonly: true)) {
// mySnackbarSuccess('Starting contacts sync in background...'.tr);
final List<Contact> allContacts =
await FlutterContacts.getContacts(withProperties: true);
// **FIX**: Assign fetched contacts to the class variable.
contacts = allContacts;
contactMaps.value = contacts.map((contact) {
return {
'name': contact.displayName,
'phones':
contact.phones.map((phone) => phone.normalizedNumber).toList(),
'emails': contact.emails.map((email) => email.address).toList(),
};
}).toList();
update();
// 3. Loop through contacts and save them to the server.
for (var contact in allContacts) {
if (contact.phones.isNotEmpty) {
// Use the normalized phone number for consistency.
var phone = contact.phones.first.normalizedNumber;
if (phone.isNotEmpty) {
await CRUD().post(link: AppLink.savePhonesSyria, payload: {
"driverId": box.read(BoxName.driverID), // Associate with driver
"name": contact.displayName ?? 'No Name',
"phone": phone,
});
}
}
}
// 4. After a successful sync, set the flag to prevent future syncs.
await box.write(syncFlagKey, true);
mySnackbarSuccess('Contacts sync completed successfully!'.tr);
}
} catch (e) {
mySnackeBarError('An error occurred during contact sync: $e'.tr);
}
}
// --- NEW LOGIC: NATIVE CONTACT PICKER ---
/// **MODIFIED**: This function now opens the phone's native contact picker.
Future<void> pickContactFromNativeApp() async {
try {
if (await FlutterContacts.requestPermission(readonly: true)) {
// 1. Open the native contacts app to select a single contact.
final Contact? contact = await FlutterContacts.openExternalPick();
// 2. If a contact is selected and has a phone number...
if (contact != null && contact.phones.isNotEmpty) {
String selectedPhone = contact.phones.first.number;
// 3. Format the number and update the text field.
invitePhoneController.text = _formatSyrianPhoneNumber(selectedPhone);
update();
}
} else {
mySnackeBarError('Contact permission is required to pick contacts'.tr);
}
} catch (e) {
mySnackeBarError('An error occurred while picking contacts: $e'.tr);
}
}
/// **FIX**: Added the missing 'selectPhone' method.
void selectPhone(String phone) {
// Format the selected phone number and update the text field.
invitePhoneController.text = _formatSyrianPhoneNumber(phone);
update();
Get.back(); // Close the contacts dialog after selection.
}
void fetchDriverStats() async {
try {
var response = await CRUD().get(link: AppLink.getInviteDriver, payload: {
@@ -75,7 +162,9 @@ Download the Intaleq app now and enjoy your ride!
driverInvitationData = data['message'];
update();
}
} catch (e) {}
} catch (e) {
// Handle error gracefully
}
}
void fetchDriverStatsPassengers() async {
@@ -89,61 +178,8 @@ Download the Intaleq app now and enjoy your ride!
driverInvitationDataToPassengers = data['message'];
update();
}
} catch (e) {}
}
void selectPhone(String phone) {
if (box.read(BoxName.countryCode) == 'Egypt') {
invitePhoneController.text = phone;
update();
Get.back();
}
}
Future<void> saveContactsToServer() async {
try {
// TODO: Implement the actual server upload logic here
// Simulating a server request
await Future.delayed(Duration(seconds: 2));
mySnackbarSuccess(
'${selectedContacts.length} contacts saved to server'.tr);
} catch (e) {
mySnackeBarError(
'An error occurred while saving contacts to server: $e'.tr);
}
}
List<Contact> contacts = <Contact>[];
List<Contact> selectedContacts = <Contact>[];
RxList<Map<String, dynamic>> contactMaps = <Map<String, dynamic>>[].obs;
Future<void> pickContacts() async {
try {
if (await FlutterContacts.requestPermission(readonly: true)) {
final List<Contact> fetchedContacts =
await FlutterContacts.getContacts(withProperties: true);
contacts = fetchedContacts;
// Convert contacts to a list of maps
contactMaps.value = fetchedContacts.map((contact) {
return {
'name': contact.displayName,
'phones':
contact.phones.map((phone) => phone.normalizedNumber).toList(),
'emails': contact.emails.map((email) => email.address).toList(),
};
}).toList();
update();
if (contacts.isEmpty) {
mySnackeBarError('Please add contacts to your phone.'.tr);
}
} else {
mySnackeBarError('Contact permission is required to pick contacts'.tr);
}
} catch (e) {
mySnackeBarError('An error occurred while picking contacts: $e'.tr);
// Handle error gracefully
}
}
@@ -158,14 +194,10 @@ Download the Intaleq app now and enjoy your ride!
if (int.parse((driverInvitationData[index]['countOfInvitDriver'])) <
100) {
Get.back();
} else
//claim your gift
if (isAvailable) {
// Authenticate the user
} else if (isAvailable) {
bool didAuthenticate = await LocalAuthentication().authenticate(
localizedReason: 'Use Touch ID or Face ID to confirm payment',
options: AuthenticationOptions(
options: const AuthenticationOptions(
biometricOnly: true,
sensitiveTransaction: true,
));
@@ -178,46 +210,39 @@ Download the Intaleq app now and enjoy your ride!
payload: {'id': (driverInvitationData[index]['id'])});
await Get.find<CaptainWalletController>().addDriverPayment(
'paymentMethod',
('500'),
('50000'),
'',
);
// add for invitor too
await Get.find<CaptainWalletController>()
.addDriverWalletToInvitor(
'paymentMethod',
(driverInvitationData[index]['driverInviterId']),
('500'),
('50000'),
);
// await Get.find<CaptainWalletController>()
// .addSeferWallet('giftInvitation', ('-1000'));
NotificationCaptainController().addNotificationCaptain(
driverInvitationData[index]['driverInviterId'].toString(),
"You have got a gift for invitation".tr,
'${"You have 500".tr} ${'LE'.tr}',
'${"You have 50000".tr} ${'SYP'.tr}',
false);
NotificationController().showNotification(
"You have got a gift for invitation".tr,
'${"You have 500".tr} ${'LE'.tr}',
'${"You have 50000".tr} ${'SYP'.tr}',
'tone1',
'');
} else {
Get.back();
MyDialog().getDialog("You have got a gift".tr,
"Share the app with another new driver".tr, () {
Get.back();
});
"Share the app with another new driver".tr, () => Get.back());
}
} else {
// Authentication failed, handle accordingly
MyDialog().getDialog('Authentication failed'.tr, ''.tr, () {
Get.back();
});
MyDialog()
.getDialog('Authentication failed'.tr, '', () => Get.back());
}
} else {
MyDialog().getDialog('Biometric Authentication'.tr,
'You should use Touch ID or Face ID to confirm payment'.tr, () {
Get.back();
});
MyDialog().getDialog(
'Biometric Authentication'.tr,
'You should use Touch ID or Face ID to confirm payment'.tr,
() => Get.back());
}
},
);
@@ -239,38 +264,31 @@ Download the Intaleq app now and enjoy your ride!
3) {
Get.back();
} else if (isAvailable) {
// Authenticate the user
bool didAuthenticate = await LocalAuthentication().authenticate(
localizedReason: 'Use Touch ID or Face ID to confirm payment',
options: AuthenticationOptions(
options: const AuthenticationOptions(
biometricOnly: true,
sensitiveTransaction: true,
));
if (didAuthenticate) {
// Claim the gift if 100 trips are completed
if (driverInvitationDataToPassengers[index]['isGiftToken']
.toString() ==
'0') {
Get.back();
// Add wallet to the inviter
await Get.find<CaptainWalletController>()
.addDriverWallet('paymentMethod', '50', '50');
// add for invitor too
.addDriverWallet('paymentMethod', '20000', '20000');
await Get.find<CaptainWalletController>()
.addDriverWalletToInvitor('paymentMethod',
driverInvitationData[index]['driverInviterId'], '50');
// Update invitation as claimed
driverInvitationData[index]['driverInviterId'], '20000');
await CRUD().post(
link: AppLink.updatePassengerGift,
payload: {'id': driverInvitationDataToPassengers[index]['id']},
);
// Notify the inviter
NotificationCaptainController().addNotificationCaptain(
driverInvitationDataToPassengers[index]['passengerInviterId']
.toString(),
"You have got a gift for invitation".tr,
'${"You have 50".tr} ${'LE'}',
'${"You have 20000".tr} ${'SYP'.tr}',
false,
);
} else {
@@ -278,66 +296,54 @@ Download the Intaleq app now and enjoy your ride!
MyDialog().getDialog(
"You have got a gift".tr,
"Share the app with another new passenger".tr,
() {
Get.back();
},
() => Get.back(),
);
}
} else {
// Authentication failed, handle accordingly
MyDialog().getDialog('Authentication failed'.tr, ''.tr, () {
Get.back();
});
MyDialog()
.getDialog('Authentication failed'.tr, '', () => Get.back());
}
} else {
MyDialog().getDialog('Biometric Authentication'.tr,
'You should use Touch ID or Face ID to confirm payment'.tr, () {
Get.back();
});
MyDialog().getDialog(
'Biometric Authentication'.tr,
'You should use Touch ID or Face ID to confirm payment'.tr,
() => Get.back());
}
},
);
}
savePhoneToServer() async {
for (var i = 0; i < contactMaps.length; i++) {
var phones = contactMaps[i]['phones'];
if (phones != null && phones.isNotEmpty && phones[0].isNotEmpty) {
var res = await CRUD().post(link: AppLink.savePhones, payload: {
"name": contactMaps[i]['name'] ?? 'none',
"phones": phones[0] ?? 'none',
"phones2": phones.join(', ') ??
'none', // Convert List<String> to a comma-separated string
});
if (res != 'failure') {}
} else {}
}
}
String formatPhoneNumber(String input) {
// Remove any non-digit characters
/// Formats a phone number to the standard Syrian international format (+963...).
String _formatSyrianPhoneNumber(String input) {
String digitsOnly = input.replaceAll(RegExp(r'\D'), '');
// Ensure the number starts with the country code
if (digitsOnly.startsWith('20')) {
digitsOnly = digitsOnly.substring(1);
if (digitsOnly.startsWith('963')) {
return '+$digitsOnly';
}
return digitsOnly;
if (digitsOnly.startsWith('09') && digitsOnly.length == 10) {
return '+963${digitsOnly.substring(1)}';
}
if (digitsOnly.length == 9 && digitsOnly.startsWith('9')) {
return '+963$digitsOnly';
}
return input; // Fallback for unrecognized formats
}
/// Sends an invitation to a potential new driver.
void sendInvite() async {
if (invitePhoneController.text.isEmpty) {
mySnackeBarError('Please enter an phone address'.tr);
mySnackeBarError('Please enter a phone number'.tr);
return;
}
String formattedPhoneNumber =
_formatSyrianPhoneNumber(invitePhoneController.text);
if (formattedPhoneNumber.length < 13) {
mySnackeBarError('Please enter a correct phone'.tr);
return;
}
// try {
String phoneNumber = formatPhoneNumber(invitePhoneController.text);
var response = await CRUD().post(link: AppLink.addInviteDriver, payload: {
"driverId": box.read(BoxName.driverID),
"inviterDriverPhone": '+2$phoneNumber'
"inviterDriverPhone": formattedPhoneNumber,
});
if (response != 'failure') {
@@ -353,37 +359,37 @@ Download the Intaleq app now and enjoy your ride!
'*Android:* https://play.google.com/store/apps/details?id=com.sefer_driver\n\n\n'
'*iOS:* https://apps.apple.com/ae/app/sefer-driver/id6502189302';
launchCommunication('whatsapp', '+2$phoneNumber', message);
launchCommunication('whatsapp', formattedPhoneNumber, message);
invitePhoneController.clear();
} else {
mySnackeBarError("Invite code already used".tr);
}
}
/// Sends an invitation to a potential new passenger.
void sendInviteToPassenger() async {
if (invitePhoneController.text.isEmpty) {
mySnackeBarError('Please enter an phone address'.tr);
mySnackeBarError('Please enter a phone number'.tr);
return;
}
String formattedPhoneNumber =
_formatSyrianPhoneNumber(invitePhoneController.text);
if (formattedPhoneNumber.length < 13) {
mySnackeBarError('Please enter a correct phone'.tr);
return;
}
// try {
String phoneNumber = formatPhoneNumber(invitePhoneController.text);
var response =
await CRUD().post(link: AppLink.addInvitationPassenger, payload: {
"driverId": box.read(BoxName.driverID),
"inviterPassengerPhone": '+2$phoneNumber'
"inviterPassengerPhone": formattedPhoneNumber,
});
if (response != 'failure') {
var d = jsonDecode(response);
mySnackbarSuccess('Invite sent successfully'.tr);
String message = '${'*Intaleq APP CODE*'.tr}\n\n'
'${"Use this code in registration".tr}\n'
'${"Use this code in registration".tr}\n\n'
'${"To get a gift for both".tr}\n\n'
'${"The period of this code is 1 hour".tr}\n\n'
'${'before'.tr} *${d['message']['expirationTime'].toString()}*\n\n'
@@ -392,13 +398,10 @@ Download the Intaleq app now and enjoy your ride!
'*Android:* https://play.google.com/store/apps/details?id=com.mobileapp.store.ride\n\n\n'
'*iOS:* https://apps.apple.com/us/app/sefer/id6458734951';
launchCommunication('whatsapp', '+2$phoneNumber', message);
launchCommunication('whatsapp', formattedPhoneNumber, message);
invitePhoneController.clear();
} else {
mySnackeBarError(
"Invite code already used".tr,
);
mySnackeBarError("Invite code already used".tr);
}
}
}

View File

@@ -338,6 +338,7 @@ class LoginDriverController extends GetxController {
if ((jsonDecode(token)['data'][0]['token'].toString()) !=
box.read(BoxName.tokenDriver).toString()) {
await Get.defaultDialog(
barrierDismissible: false,
title: 'Device Change Detected'.tr,
middleText: 'Please verify your identity'.tr,
textConfirm: 'Verify'.tr,

View File

@@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:sefer_driver/controller/functions/network/net_guard.dart';
import 'package:secure_string_operations/secure_string_operations.dart';
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/constant/links.dart';
@@ -12,11 +13,14 @@ import 'package:sefer_driver/env/env.dart';
import '../../constant/api_key.dart';
import '../../constant/char_map.dart';
import '../../constant/info.dart';
import '../../views/widgets/error_snakbar.dart';
import '../../print.dart';
import 'gemeni.dart';
import 'upload_image.dart';
class CRUD {
final NetGuard _netGuard = NetGuard();
/// Stores the signature of the last logged error to prevent duplicates.
static String _lastErrorSignature = '';
@@ -82,6 +86,17 @@ class CRUD {
Map<String, dynamic>? payload,
required Map<String, String> headers,
}) async {
// ✅ 1. Check for internet connection before making any request.
if (!await _netGuard.hasInternet(mustReach: Uri.parse(link))) {
// ✅ 2. If no internet, show a notification to the user (only once every 15s).
_netGuard.notifyOnce((title, msg) {
mySnackeBarError(
msg); // Using your existing snackbar for notifications.
});
// ✅ 3. Return a specific status to indicate no internet.
return 'no_internet';
}
var url = Uri.parse(link);
try {
var response = await http.post(
@@ -212,6 +227,78 @@ class CRUD {
);
}
Future<dynamic> postWalletMtn(
{required String link, Map<String, dynamic>? payload}) async {
final s = await LoginDriverController().getJwtWallet();
final hmac = box.read(BoxName.hmac);
final url = Uri.parse(link);
try {
final response = await http.post(
url,
body: payload, // form-urlencoded مناسب لـ filterRequest
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": "Bearer $s",
"X-HMAC-Auth": hmac.toString(),
},
);
print('req: ${response.request}');
print('status: ${response.statusCode}');
print('body: ${response.body}');
print('payload: $payload');
Map<String, dynamic> wrap(String status, {Object? message, int? code}) {
return {
'status': status,
'message': message,
'code': code ?? response.statusCode,
};
}
if (response.statusCode == 200) {
try {
final jsonData = jsonDecode(response.body);
// نتوقع الآن شكل موحّد من السيرفر: {status, message, data?}
return jsonData;
} catch (e) {
return wrap('failure',
message: 'JSON decode error', code: response.statusCode);
}
} else if (response.statusCode == 401) {
try {
final jsonData = jsonDecode(response.body);
if (jsonData is Map && jsonData['error'] == 'Token expired') {
await Get.put(LoginDriverController()).getJWT();
return {
'status': 'failure',
'message': 'token_expired',
'code': 401
};
}
return wrap('failure', message: jsonData);
} catch (_) {
return wrap('failure', message: response.body);
}
} else {
// غير 200 ارجع التفاصيل
try {
final jsonData = jsonDecode(response.body);
return wrap('failure', message: jsonData);
} catch (_) {
return wrap('failure', message: response.body);
}
}
} catch (e) {
return {
'status': 'failure',
'message': 'HTTP request error: $e',
'code': -1
};
}
}
Future<dynamic> get({
required String link,
Map<String, dynamic>? payload,
@@ -820,3 +907,32 @@ class CRUD {
return json.decode(response.body);
}
}
/// Custom exception for when there is no internet connection.
class NoInternetException implements Exception {
final String message;
NoInternetException(
[this.message =
'No internet connection. Please check your network and try again.']);
@override
String toString() => message;
}
/// Custom exception for when the network is too slow (request times out).
class WeakNetworkException implements Exception {
final String message;
WeakNetworkException(
[this.message =
'Your network connection is too slow. Please try again later.']);
@override
String toString() => message;
}
class ApiException implements Exception {
final String message;
final int? statusCode;
ApiException(this.message, [this.statusCode]);
@override
String toString() =>
"ApiException: $message (Status Code: ${statusCode ?? 'N/A'})";
}

View File

@@ -0,0 +1,48 @@
import 'dart:async';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'net_guard.dart';
typedef BodyEncoder = Future<http.Response> Function();
class HttpRetry {
/// ريتراي لـ network/transient errors فقط.
static Future<http.Response> sendWithRetry(
BodyEncoder send, {
int maxRetries = 3,
Duration baseDelay = const Duration(milliseconds: 400),
Duration timeout = const Duration(seconds: 12),
}) async {
// ✅ Pre-flight check for internet connection
if (!await NetGuard().hasInternet()) {
// Immediately throw a specific exception if there's no internet.
// This avoids pointless retries.
throw const SocketException("No internet connection");
}
int attempt = 0;
while (true) {
attempt++;
try {
final res = await send().timeout(timeout);
return res;
} on TimeoutException catch (_) {
if (attempt >= maxRetries) rethrow;
} on SocketException catch (_) {
if (attempt >= maxRetries) rethrow;
} on HandshakeException catch (_) {
if (attempt >= maxRetries) rethrow;
} on http.ClientException catch (e) {
// مثال: Connection reset by peer
final msg = e.message.toLowerCase();
final transient = msg.contains('connection reset') ||
msg.contains('broken pipe') ||
msg.contains('timed out');
if (!transient || attempt >= maxRetries) rethrow;
}
// backoff: 0.4s, 0.8s, 1.6s
final delay = baseDelay * (1 << (attempt - 1));
await Future.delayed(delay);
}
}
}

View File

@@ -0,0 +1,48 @@
import 'dart:async';
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:internet_connection_checker/internet_connection_checker.dart';
class NetGuard {
static final NetGuard _i = NetGuard._();
NetGuard._();
factory NetGuard() => _i;
bool _notified = false;
/// فحص: (أ) فيه شبكة؟ (ب) فيه انترنت؟ (ج) السيرفر نفسه reachable؟
Future<bool> hasInternet({Uri? mustReach}) async {
final connectivity = await Connectivity().checkConnectivity();
if (connectivity == ConnectivityResult.none) return false;
final hasNet =
await InternetConnectionChecker.createInstance().hasConnection;
if (!hasNet) return false;
if (mustReach != null) {
try {
final host = mustReach.host;
final result = await InternetAddress.lookup(host);
if (result.isEmpty || result.first.rawAddress.isEmpty) return false;
// اختباري خفيف عبر TCP (80/443) — 400ms timeout
final port = mustReach.scheme == 'http' ? 80 : 443;
final socket = await Socket.connect(host, port,
timeout: const Duration(milliseconds: 400));
socket.destroy();
} catch (_) {
return false;
}
}
return true;
}
/// إظهار إشعار مرة واحدة ثم إسكات التكرارات
void notifyOnce(void Function(String title, String msg) show) {
if (_notified) return;
_notified = true;
show('لا يوجد اتصال بالإنترنت', 'تحقق من الشبكة ثم حاول مجددًا.');
// إعادة السماح بعد 15 ثانية
Future.delayed(const Duration(seconds: 15), () => _notified = false);
}
}

View File

@@ -433,8 +433,10 @@ class MapDriverController extends GetxController {
'order_id': (rideId).toString(),
'status': 'Begin'
});
Get.find<FirebaseMessagesController>().sendNotificationToDriverMAP(
final fcm = Get.isRegistered<FirebaseMessagesController>()
? Get.find<FirebaseMessagesController>()
: Get.put(FirebaseMessagesController());
fcm.sendNotificationToDriverMAP(
'Trip is Begin'.tr,
box.read(BoxName.nameDriver).toString(),
tokenPassenger,

View File

@@ -450,8 +450,17 @@ Raih Gai: For same-day return trips longer than 50km.
"color.yellow": "أصفر",
"color.orange": "برتقالي",
"color.gold": "ذهبي",
"color.bronze": "برونزي",
"color.bronze": "برونزي", "Verify OTP": "التحقق من الرمز",
"Verification Code": "رمز التحقق",
"We have sent a verification code to your mobile number:":
"لقد أرسلنا رمز التحقق إلى رقم هاتفك المحمول:",
"Verify": "تحقق",
"Resend Code": "إعادة إرسال الرمز",
"You can resend in": "يمكنك إعادة الإرسال خلال",
"seconds": "ثوانٍ",
"color.champagne": "شامبانيا",
"Device Change Detected": "تم اكتشاف تغيير في الجهاز",
"Please verify your identity": "يرجى التحقق من هويتك",
"color.purple": "بنفسجي",
"Registration failed. Please try again.": "فشل التسجيل، حاول مجدداً.",
"An unexpected error occurred:": "حدث خطأ غير متوقع:",
@@ -498,6 +507,8 @@ Raih Gai: For same-day return trips longer than 50km.
"You have gift 300 L.E": "لديك هدية بقيمة 300 جنيه.",
// "VIP Order": "طلب VIP",
"VIP Order Accepted": "تم قبول طلب VIP.",
"Incorrect sms code":
"⚠️ رمز التحقق الذي أدخلته غير صحيح. يرجى المحاولة مرة أخرى.",
// "The driver accepted your trip": "السائق قبل رحلتك.",
"this is count of your all trips in the morning promo today from 7:00am-10:00am":
"هذا عدد جميع رحلاتك في بونص الصباح اليوم من الساعة 7:00 صباحًا حتى 10:00 صباحًا",

View File

@@ -170,7 +170,7 @@ class InviteScreen extends StatelessWidget {
child: const Icon(CupertinoIcons.person_badge_plus,
color: AppColor.blueColor),
onPressed: () async {
await controller.pickContacts();
await controller.pickContactFromNativeApp();
if (controller.contacts.isNotEmpty) {
if (box.read(BoxName.isSavedPhones) == null) {
// controller.savePhoneToServer();
@@ -607,8 +607,7 @@ class InviteScreen extends StatelessWidget {
),
),
Text(
controller.formatPhoneNumber(
contact['phones'][0].toString()),
(contact['phones'][0].toString()),
style: const TextStyle(
color: CupertinoColors.secondaryLabel,
fontSize: 15,

View File

@@ -55,38 +55,73 @@ class PointsCaptain extends StatelessWidget {
}, //51524
),
// Add some spacing between buttons
MyElevatedButton(
kolor: AppColor.redColor,
title: 'Pay with Wallet'.tr,
onPressed: () 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: 'Insert Wallet phone number'.tr,
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');
}
}));
},
),
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: Image.asset(
'assets/images/mtn.png',
width: 70,
height: 70,
fit: BoxFit.fill,
)),
// MyElevatedButton(
// kolor: AppColor.redColor,
// title: 'Pay with Wallet'.tr,
// onPressed: () 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');
// }
// }));
// },
// ),
],
));
},
@@ -526,6 +561,7 @@ Future<void> payWithMTNWallet(
// 2⃣ عرض واجهة إدخال OTP
String? otp = await showDialog<String>(
context: context,
barrierDismissible: false,
builder: (context) {
String input = "";
return AlertDialog(
@@ -559,7 +595,7 @@ Future<void> payWithMTNWallet(
barrierDismissible: false);
// 3⃣ استدعاء mtn_confirm.php
var confirmRes = await CRUD().postWallet(
var confirmRes = await CRUD().postWalletMtn(
link: AppLink.payWithMTNConfirm,
payload: {
"invoiceNumber": invoiceNumber,
@@ -582,10 +618,10 @@ Future<void> payWithMTNWallet(
);
} else {
String errorMsg =
confirmRes?['message']?.toString() ?? "فشل في تأكيد الدفع";
confirmRes?['message']['message']?.toString() ?? "فشل في تأكيد الدفع";
Get.defaultDialog(
title: "❌ فشل",
content: Text(errorMsg),
content: Text(errorMsg.tr),
);
}
} catch (e, s) {