25-9-8-1
This commit is contained in:
@@ -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==========================
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'})";
|
||||
}
|
||||
|
||||
48
lib/controller/functions/network/connection_check.dart
Normal file
48
lib/controller/functions/network/connection_check.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
48
lib/controller/functions/network/net_guard.dart
Normal file
48
lib/controller/functions/network/net_guard.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 صباحًا",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user