feat: N1d/N2/N3 + قانون فلاتر وإعادة ضبط تطبيق السائق

الباك إند:
- تعيين أول أدمن صار يُنشئ المستخدم إن لم يوجد — كان يشترط دخولاً سابقاً،
  وهي بيضة ودجاجة تمنع الدخول إلى لوحة أي مستأجر جديد أصلاً.
- OTP_DEV_MODE كان يفشل مفتوحاً (`!== 'false'`): غياب المتغيّر أو خطأ مطبعي
  يترك الإنتاج برمز ثابت يفتح كل حساب، ويحرس السحب المالي كذلك. صار
  `=== 'true'` في الإعداد وفي قارئَيه، مع تحذير عند الإقلاع.
- N2: التحقّق من اللوغو عند الرفع (PNG/JPEG · 512+ · مربّع · سقف 5MB) عبر
  قراءة الترويسة بلا اعتمادية — بدل اكتشاف أيقونة ممطوطة بعد النشر.

N3: السكربت يولّد build_config.dart بأعلام const (طبقات docs/22 §1.5)،
ويضبط bundle IDs واسم التطبيق والأيقونات/splash، و--build يشغّل Shorebird.

فلاتر:
- docs/23: قانون مُلزِم للتطبيقين (البنية · Cubit · طبقة الشبكة · الأعلام).
- السائق: أُزيل Dart القديم (GetX) مع الإبقاء على الأصيل والإضافات وشهادات
  التوقيع وShorebird وFirebase الخاص به، وأُعيد هيكلته مطابقاً للراكب.
- طبقة الشبكة ترسل x-device-id وتجدّد التوكن عند 401 مرة واحدة فقط.
- الحزم موحّدة بين التطبيقين، والـAPI https حصراً في الاثنين.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-18 13:06:46 +03:00
co-authored by Claude Fable 5
parent 3ab74a13a8
commit de4bbd00ac
271 changed files with 1333 additions and 81068 deletions
@@ -1,61 +0,0 @@
import 'dart:convert';
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
import 'package:get/get.dart';
import '../../../constant/box_name.dart';
import '../../../constant/links.dart';
import '../../../main.dart';
import '../../../views/home/Captin/history/history_details_page.dart';
import '../../functions/crud.dart';
import '../../functions/encrypt_decrypt.dart';
class HistoryCaptainController extends GetxController {
bool isloading = false;
Map historyData = {};
Map historyDetailsData = {};
late String orderID;
getOrderId(String orderId) {
orderID = orderId;
update();
}
getHistory() async {
isloading = true;
var res = await CRUD().get(
link: AppLink.getDriverOrder,
payload: {'driver_id': box.read(BoxName.driverID)});
if (res != 'failure') {
historyData = jsonDecode(res);
isloading = false;
update();
} else {
Get.defaultDialog(
title: 'No ride yet'.tr,
middleText: '',
barrierDismissible: false,
confirm: MyElevatedButton(
title: 'Back'.tr,
onPressed: () {
Get.back();
Get.back();
}));
}
}
getHistoryDetails(String orderId) async {
isloading = true;
var res = await CRUD()
.get(link: AppLink.getRideOrderID, payload: {'id': (orderId)});
historyDetailsData = jsonDecode(res);
isloading = false;
update();
Get.to(() => HistoryDetailsPage());
}
@override
void onInit() {
getHistory();
super.onInit();
}
}
@@ -1,457 +0,0 @@
import 'dart:convert';
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 '../../firebase/local_notification.dart';
import '../../functions/launch.dart';
import '../../notification/notification_captain_controller.dart';
class InviteController extends GetxController {
final TextEditingController invitePhoneController = TextEditingController();
List driverInvitationData = [];
List driverInvitationDataToPassengers = [];
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) {
selectedTab = index;
update();
}
Future<void> shareDriverCode() async {
if (driverCouponCode != null) {
final String shareText =
'''Join Intaleq as a driver using my referral code!
Use code: $driverCouponCode
Download the Intaleq Driver app now and earn rewards!
''';
await Share.share(shareText);
}
}
Future<void> sharePassengerCode() async {
if (couponCode != null) {
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!
''';
await Share.share(shareText);
}
}
@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) {
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: {
"driverId": box.read(BoxName.driverID),
});
if (response != 'failure') {
var data = jsonDecode(response);
driverInvitationData = data['message'];
update();
}
} catch (e) {
// Handle error gracefully
}
}
void fetchDriverStatsPassengers() async {
try {
var response = await CRUD()
.get(link: AppLink.getDriverInvitationToPassengers, payload: {
"driverId": box.read(BoxName.driverID),
});
if (response != 'failure') {
var data = jsonDecode(response);
driverInvitationDataToPassengers = data['message'];
update();
}
} catch (e) {
// Handle error gracefully
}
}
void onSelectDriverInvitation(int index) async {
MyDialog().getDialog(
int.parse((driverInvitationData[index]['countOfInvitDriver'])) < 100
? '${'When'.tr} ${(driverInvitationData[index]['invitorName'])} ${"complete, you can claim your gift".tr} '
: 'You deserve the gift'.tr,
'${(driverInvitationData[index]['invitorName'])} ${(driverInvitationData[index]['countOfInvitDriver'])} / 100 ${'Trip'.tr}',
() async {
bool isAvailable = await LocalAuthentication().isDeviceSupported();
if (int.parse((driverInvitationData[index]['countOfInvitDriver'])) <
100) {
Get.back();
} else if (isAvailable) {
bool didAuthenticate = await LocalAuthentication().authenticate(
localizedReason: 'Use Touch ID or Face ID to confirm payment',
options: const AuthenticationOptions(
biometricOnly: true,
sensitiveTransaction: true,
));
if (didAuthenticate) {
if ((driverInvitationData[index]['isGiftToken']).toString() ==
'0') {
Get.back();
await CRUD().post(
link: AppLink.updateInviteDriver,
payload: {'id': (driverInvitationData[index]['id'])});
await Get.find<CaptainWalletController>().addDriverPayment(
'paymentMethod',
('50000'),
'',
);
await Get.find<CaptainWalletController>()
.addDriverWalletToInvitor(
'paymentMethod',
(driverInvitationData[index]['driverInviterId']),
('50000'),
);
NotificationCaptainController().addNotificationCaptain(
driverInvitationData[index]['driverInviterId'].toString(),
"You have got a gift for invitation".tr,
'${"You have 50000".tr} ${'SYP'.tr}',
false);
NotificationController().showNotification(
"You have got a gift for invitation".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());
}
} else {
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());
}
},
);
}
void onSelectPassengerInvitation(int index) async {
bool isAvailable = await LocalAuthentication().isDeviceSupported();
MyDialog().getDialog(
int.parse(driverInvitationDataToPassengers[index]['countOfInvitDriver']
.toString()) <
3
? '${'When'.tr} ${(driverInvitationDataToPassengers[index]['passengerName'].toString())} ${"complete, you can claim your gift".tr} '
: 'You deserve the gift'.tr,
'${(driverInvitationDataToPassengers[index]['passengerName'].toString())} ${driverInvitationDataToPassengers[index]['countOfInvitDriver']} / 3 ${'Trip'.tr}',
() async {
if (int.parse(driverInvitationDataToPassengers[index]
['countOfInvitDriver']
.toString()) <
3) {
Get.back();
} else if (isAvailable) {
bool didAuthenticate = await LocalAuthentication().authenticate(
localizedReason: 'Use Touch ID or Face ID to confirm payment',
options: const AuthenticationOptions(
biometricOnly: true,
sensitiveTransaction: true,
));
if (didAuthenticate) {
if (driverInvitationDataToPassengers[index]['isGiftToken']
.toString() ==
'0') {
Get.back();
await Get.find<CaptainWalletController>()
.addDriverWallet('paymentMethod', '20000', '20000');
await Get.find<CaptainWalletController>()
.addDriverWalletToInvitor('paymentMethod',
driverInvitationData[index]['driverInviterId'], '20000');
await CRUD().post(
link: AppLink.updatePassengerGift,
payload: {'id': driverInvitationDataToPassengers[index]['id']},
);
NotificationCaptainController().addNotificationCaptain(
driverInvitationDataToPassengers[index]['passengerInviterId']
.toString(),
"You have got a gift for invitation".tr,
'${"You have 20000".tr} ${'SYP'.tr}',
false,
);
} else {
Get.back();
MyDialog().getDialog(
"You have got a gift".tr,
"Share the app with another new passenger".tr,
() => Get.back(),
);
}
} else {
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());
}
},
);
}
/// Formats a phone number to the standard Syrian international format (+963...).
String _formatSyrianPhoneNumber(String input) {
String digitsOnly = input.replaceAll(RegExp(r'\D'), '');
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
}
String normalizeSyrianPhone(String input) {
String phone = input.trim();
// احذف كل شيء غير أرقام
phone = phone.replaceAll(RegExp(r'[^0-9]'), '');
// إذا يبدأ بـ 0 → احذفها
if (phone.startsWith('0')) {
phone = phone.substring(1);
}
// إذا يبدأ بـ 963 مكررة → احذف التكرار
while (phone.startsWith('963963')) {
phone = phone.substring(3);
}
// إذا يبدأ بـ 963 ولكن داخله كمان 963 → خليه مرة واحدة فقط
if (phone.startsWith('963') && phone.length > 12) {
phone = phone.substring(phone.length - 9); // آخر 9 أرقام
}
// الآن إذا كان بلا 963 → أضفها
if (!phone.startsWith('963')) {
phone = '963' + phone;
}
return phone;
}
/// Sends an invitation to a potential new driver.
void sendInvite() async {
if (invitePhoneController.text.isEmpty) {
mySnackeBarError('Please enter a phone number'.tr);
return;
}
// Format Syrian phone number: remove leading 0 and add +963
String formattedPhoneNumber =
normalizeSyrianPhone(invitePhoneController.text);
if (formattedPhoneNumber.length != 12) {
mySnackeBarError('Please enter a correct phone'.tr);
return;
}
var response = await CRUD().post(link: AppLink.addInviteDriver, payload: {
"driverId": box.read(BoxName.driverID),
"inviterDriverPhone": formattedPhoneNumber,
});
if (response != 'failure') {
var d = (response);
mySnackbarSuccess('Invite sent successfully'.tr);
String message = '${'*Intaleq DRIVER CODE*'.tr}\n\n'
'${"Use this code in registration".tr}\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'
'_*${d['message']['inviteCode'].toString()}*_\n\n'
'${"Install our app:".tr}\n'
'*Android:* https://play.google.com/store/apps/details?id=com.intaleq_driver \n\n\n'
'*iOS:* https://apps.apple.com/st/app/intaleq-driver/id6482995159';
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 a phone number'.tr);
return;
}
// Format Syrian phone number: remove leading 0 and add +963
String formattedPhoneNumber = invitePhoneController.text.trim();
if (formattedPhoneNumber.startsWith('0')) {
formattedPhoneNumber = formattedPhoneNumber.substring(1);
}
formattedPhoneNumber = '+963$formattedPhoneNumber';
if (formattedPhoneNumber.length < 12) {
// +963 + 9 digits = 12+
mySnackeBarError('Please enter a correct phone'.tr);
return;
}
var response = await CRUD().post(
link: AppLink.addInvitationPassenger,
payload: {
"driverId": box.read(BoxName.driverID),
"inviterPassengerPhone": formattedPhoneNumber,
},
);
if (response != 'failure') {
var d = response;
mySnackbarSuccess('Invite sent successfully'.tr);
String message = '${'*Intaleq APP CODE*'.tr}\n\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'
'_*${d['message']['inviteCode'].toString()}*_\n\n'
'${"Install our app:".tr}\n'
'*Android:* https://play.google.com/store/apps/details?id=com.Intaleq.intaleq\n\n\n'
'*iOS:* https://apps.apple.com/st/app/intaleq-rider/id6748075179';
launchCommunication('whatsapp', formattedPhoneNumber, message);
invitePhoneController.clear();
} else {
mySnackeBarError("Invite code already used".tr);
}
}
}
class PassengerStats {
final int totalInvites;
final int activeUsers;
final double totalEarnings;
PassengerStats({
this.totalInvites = 0,
this.activeUsers = 0,
this.totalEarnings = 0.0,
});
}
@@ -1,787 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'dart:math';
import 'package:http/http.dart' as http;
import 'package:permission_handler/permission_handler.dart';
import 'package:sefer_driver/views/auth/captin/egypt_card_a_i.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get/get.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/main.dart';
import 'package:sefer_driver/views/home/Captin/home_captain/home_captin.dart';
import 'package:location/location.dart';
import '../../../constant/api_key.dart';
import '../../../print.dart';
import '../../../views/auth/captin/otp_page.dart';
import '../../../views/auth/captin/otp_token_page.dart';
import '../../../views/auth/syria/pending_driver_page.dart';
import '../../firebase/local_notification.dart';
import '../../firebase/notification_service.dart';
import '../../functions/encrypt_decrypt.dart';
import '../../functions/package_info.dart';
import '../../functions/security_checks.dart';
class LoginDriverController extends GetxController {
final formKey = GlobalKey<FormState>();
TextEditingController emailController = TextEditingController();
TextEditingController phoneController = TextEditingController();
TextEditingController passwordController = TextEditingController();
TextEditingController passwordController2 = TextEditingController();
bool isAgreeTerms = false;
bool isGoogleDashOpen = false;
bool isGoogleLogin = false;
bool isloading = false;
late int isTest = 1;
final FlutterSecureStorage _storage = const FlutterSecureStorage();
final location = Location();
void changeAgreeTerm() {
isAgreeTerms = !isAgreeTerms;
update();
}
bool isPasswordHidden = true;
void togglePasswordVisibility() {
isPasswordHidden = !isPasswordHidden;
update([
'passwordVisibility'
]); // Use a unique ID to only update the password field
}
void changeGoogleDashOpen() {
isGoogleDashOpen = !isGoogleDashOpen;
update();
}
@override
void onInit() async {
box.write(BoxName.countryCode, 'Egypt');
// box.write(BoxName.driverID, '34feffd3fa72d6bee56b');
// await getAppTester();
getJWT();
super.onInit();
}
// getAppTester() async {
// var res = await CRUD().get(
// link: AppLink.getTesterApp,
// payload: {'appPlatform': AppInformation.appName});
// // Log.print('res: ${res}');
// if (res != 'failure') {
// var d = jsonDecode(res);
// isTest = d['message'][0]['isTest'];
// // Log.print('isTest: ${isTest}');
// box.write(BoxName.isTest, isTest);
// // Log.print('isTest: ${box.read(BoxName.isTest)}');
// update();
// } else {
// isTest = 0;
// box.write(BoxName.isTest, isTest);
// update();
// return false;
// }
// }
updateAppTester(String appPlatform) async {
await CRUD().post(
link: AppLink.updateTesterApp, payload: {'appPlatform': appPlatform});
}
isPhoneVerified() async {
// 1. أولاً نتأكد هل يوجد رقم هاتف محفوظ محلياً (تجاوز الـ OTP)
String? savedPhone = box.read(BoxName.phoneDriver);
Log.print('savedPhone: ${savedPhone}');
if (savedPhone == null || savedPhone.isEmpty) {
// لا يوجد رقم محفوظ، نعيد المستخدم لصفحة إدخال الرقم
Get.offAll(() => PhoneNumberScreen());
return;
}
var res = await CRUD().post(
link: AppLink.isPhoneVerified, payload: {'phone_number': savedPhone});
Log.print('isPhoneVerified Res: $res');
if (res != 'failure') {
// الحالة 1: السائق موجود ومفعل -> نذهب للصفحة التالية
Get.offAll(() => EgyptCardAI());
} else {
// الحالة 2: الرد فشل (السائق غير موجود في الداتا بيز)
if (savedPhone != null && savedPhone.isNotEmpty) {
// بما أن الرقم محفوظ، فهذا مستخدم جديد أكمل التحقق
// نوجهه لصفحة التسجيل/البطاقة بدلاً من العودة للبداية
Log.print('New User Detected, redirecting to Registration...');
Get.offAll(() => EgyptCardAI());
} else {
// لا يوجد رقم محفوظ ولا مستخدم في السيرفر -> عودة للبداية
Get.offAll(() => PhoneNumberScreen());
}
}
}
// isPhoneVerified() async {
// var res = await CRUD().post(
// link: AppLink.isPhoneVerified,
// payload: {'phone_number': box.read(BoxName.phoneDriver)});
// Log.print('isPhoneVerified: ${res}');
// if (res != 'failure') {
// // Get.offAll(() => SyrianCardAI());
// Get.offAll(() => EgyptCardAI());
// // isloading = false;
// // update();
// } else {
// Get.offAll(() => PhoneNumberScreen());
// }
// }
void saveAgreementTerms() {
box.write(BoxName.agreeTerms, 'agreed');
update();
}
void saveCountryCode(String countryCode) {
box.write(BoxName.countryCode, countryCode);
update();
}
var dev = '';
getJwtWallet() async {
if (box.read(BoxName.security_check).toString() != 'passed') {
Log.print('Security check failed');
return;
}
Log.print('Security check passed');
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
// Log.print('fingerPrint: ${fingerPrint}');
dev = Platform.isAndroid ? 'android' : 'ios';
var payload = {
'id': box.read(BoxName.driverID),
'password': AK.passnpassenger,
'aud': '${AK.allowedWallet}$dev',
'fingerPrint': fingerPrint
};
var response1 = await http.post(
Uri.parse(AppLink.loginJwtWalletDriver),
body: payload,
);
// Log.print('response.request: ${response1.request}');
// Log.print('response.JwtWallet: ${response1.body}');
// print(payload);
// Log.print('payment["jwt"]: ${jsonDecode(response1.body)['jwt']}');
await box.write(BoxName.hmac, jsonDecode(response1.body)['hmac']);
return jsonDecode(response1.body)['jwt'].toString();
}
String shortHash(String password) {
var bytes = utf8.encode(password);
var digest = sha256.convert(bytes);
return base64UrlEncode(digest.bytes);
}
getJWT() async {
dev = Platform.isAndroid ? 'android' : 'ios';
Log.print(
'box.read(BoxName.firstTimeLoadKey): ${box.read(BoxName.firstTimeLoadKey)}');
if (box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
var payload = {
'id': box.read(BoxName.driverID) ?? AK.newId,
'password': AK.passnpassenger,
'aud': '${AK.allowed}$dev',
};
// Log.print('payload: ${payload}');
var response0 = await http.post(
Uri.parse(AppLink.loginFirstTimeDriver),
body: payload,
);
// Log.print('response0: ${response0.body}');
Log.print('request: ${response0.request}');
if (response0.statusCode == 200) {
final decodedResponse1 = jsonDecode(response0.body);
// Log.print('decodedResponse1: ${decodedResponse1}');
final jwt = decodedResponse1['jwt'];
box.write(BoxName.jwt, c(jwt));
// ✅ بعد التأكد أن كل المفاتيح موجودة
await EncryptionHelper.initialize();
// await AppInitializer().getKey();
} else {}
} else {
await EncryptionHelper.initialize();
var payload = {
'id': box.read(BoxName.driverID),
'password': box.read(BoxName.emailDriver),
'aud': '${AK.allowed}$dev',
};
// print(payload);
var response1 = await http.post(
Uri.parse(AppLink.loginJwtDriver),
body: payload,
);
// print(response1.request);
// print(response1.body);
if (response1.statusCode == 200) {
final decodedResponse1 = jsonDecode(response1.body);
// Log.print('decodedResponse1: ${decodedResponse1}');
final jwt = decodedResponse1['jwt'];
await box.write(BoxName.jwt, c(jwt));
// await AppInitializer().getKey();
}
}
}
Future<void> getLocationPermission() async {
var status = await Permission.locationAlways.status;
if (!status.isGranted) {
await Permission.locationAlways.request();
}
update();
}
String generateUniqueIdFromEmail(String email) {
// Step 1: Extract the local part of the email
String localPart = email.split('@')[0];
// Step 2: Replace invalid characters (if any)
String cleanLocalPart = localPart.replaceAll(RegExp(r'[^a-zA-Z0-9]'), '');
// Step 3: Ensure it does not exceed 24 characters
if (cleanLocalPart.length > 24) {
cleanLocalPart = cleanLocalPart.substring(0, 24);
}
// Step 4: Generate a random suffix if needed
String suffix = generateRandomSuffix(24 - cleanLocalPart.length);
return cleanLocalPart + suffix;
}
String generateRandomSuffix(int length) {
const String chars =
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
Random random = Random();
return List.generate(length, (index) => chars[random.nextInt(chars.length)])
.join('');
}
bool isInviteDriverFound = false;
Future updateInvitationCodeFromRegister() async {
var res = await CRUD().post(
link: AppLink.updateDriverInvitationDirectly,
payload: {
"inviterDriverPhone": box.read(BoxName.phoneDriver).toString(),
},
);
Log.print('invite: ${res}');
box.write(BoxName.isInstall, '1');
// ✅ تصحيح 1: التحقق من أن الرد ليس 'failure' وأنه Map قبل القراءة
if (res != 'failure' && res is Map && res['status'] == 'success') {
isInviteDriverFound = true;
update();
NotificationController().showNotification(
"Code approved".tr, "Code approved".tr, 'tone2', '');
// ✅ تصحيح 2: استخراج التوكن حسب هيكل PHP الجديد (مفتاح 'data')
String? targetToken;
// الهيكل الجديد: {status: success, data: {token: ...}}
if (res['data'] != null && res['data']['token'] != null) {
targetToken = res['data']['token'].toString();
}
// دعم الهيكل القديم للاحتياط: {status: success, message: [{token: ...}]}
else if (res['message'] is List && res['message'].isNotEmpty) {
targetToken = res['message'][0]['token'].toString();
}
if (targetToken != null) {
NotificationService.sendNotification(
target: targetToken,
title: 'You have received a gift token!'.tr,
body: 'for '.tr + box.read(BoxName.phoneDriver).toString(),
isTopic: false,
tone: 'tone2',
driverList: [],
category: 'You have received a gift token!',
);
}
} else {
// فشل التحديث (الكود غير موجود أو خطأ سيرفر)
// لا تفعل شيئاً أو اعرض رسالة خطأ صامتة
}
}
loginWithGoogleCredential(String driverID, String email) async {
isloading = true;
update();
try {
// 1. تجهيز البيانات الإضافية للأمان
String platformName = Platform.isIOS ? 'ios' : 'android';
String storedPhone = box.read(BoxName.phoneDriver)?.toString() ?? '';
// 2. إرسال الطلب الموحد (Login + Token + PackageInfo)
var res = await CRUD().get(link: AppLink.loginFromGoogleCaptin, payload: {
'id': driverID,
'email': email, // ضروري للمطابقة الأمنية في السيرفر
'phone': storedPhone, // اختياري، لزيادة الموثوقية
'platform': platformName // لجلب معلومات التحديث المناسبة للجهاز
});
Log.print('loginWithGoogleCredential Response: $res');
if (res == 'failure') {
// فشل الدخول (البيانات غير موجودة أو غير مطابقة)
// نحاول التحقق من حالة الهاتف لتوجيهه للتسجيل إذا كان جديداً
await isPhoneVerified();
isloading = false;
update();
return false;
} else {
var jsonResponse = jsonDecode(res);
if (jsonResponse['status'] == 'success') {
// ---------------------------------------------------------
// A. معالجة بيانات السائق الأساسية
// ---------------------------------------------------------
var d = jsonResponse['data'][0];
if (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']);
if (box.read(BoxName.isInstall).toString() == 'null' ||
box.read(BoxName.isInstall).toString() == '') {
box.write(BoxName.isInstall, d['isInstall']);
}
Log.print(
'isInstall for update invitation: ${box.read(BoxName.isInstall)}');
// box.write(BoxName.isInstall, d['isInstall']);
box.write(BoxName.nameArabic,
d['name_arabic']); // تأكد أن هذا الحقل موجود في الرد أو عدله
box.write(BoxName.bankCodeDriver, d['bankCode']);
box.write(BoxName.accountBankNumberDriver, d['accountBank']);
box.write(
BoxName.nameDriver, '${d['first_name']} ${d['last_name']}');
int categoryId =
int.tryParse(d['vehicle_category_id']?.toString() ?? '0') ?? 0;
// معالجة سنة السيارة بشكل آمن لتجنب الكراش
int carYear = int.tryParse(d['year']?.toString() ?? '0') ?? 0;
box.write(BoxName.carYear, carYear);
// منطق تحديد نوع السيارة
String gender = d['gender']?.toString() ?? '';
if (categoryId == 2) {
// 2 = دراجة / سكوتر (حسب قاعدة البيانات)
if (gender == 'Male') {
box.write(BoxName.carTypeOfDriver, 'Scooter');
} else {
box.write(BoxName.carTypeOfDriver, 'Pink Bike');
}
} else if (categoryId == 3) {
// 3 = فان (إذا كان لديكم فئة فان)
box.write(BoxName.carTypeOfDriver, 'Van');
} else if (carYear > 2017) {
if (gender != 'Male') {
box.write(BoxName.carTypeOfDriver, 'Lady');
} else {
box.write(BoxName.carTypeOfDriver, 'Comfort');
}
} else if (carYear > 2002 && carYear <= 2017) {
box.write(BoxName.carTypeOfDriver, 'Speed');
} else if (carYear <= 2002 && carYear > 0) {
box.write(BoxName.carTypeOfDriver, 'Awfar Car');
}
// ---------------------------------------------------------
// B. معالجة التحديثات (Package Info) من نفس الرد
// ---------------------------------------------------------
// if (jsonResponse['package_info'] != null &&
// (jsonResponse['package_info'] as List).isNotEmpty) {
// var pkg = jsonResponse['package_info'][0];
// // استدعاء دالة فحص التحديث (تأكد من وجودها أو مرر البيانات لها)
// // checkUpdateFromData(pkg);
// checkForUpdate(Get.context!);
// }
// ---------------------------------------------------------
// C. معالجة كود الدعوة
// ---------------------------------------------------------
if (box.read(BoxName.isInstall) == null ||
box.read(BoxName.isInstall).toString() == '0') {
updateInvitationCodeFromRegister();
}
// ---------------------------------------------------------
// D. التحقق الأمني (Device Fingerprint & Token)
// ---------------------------------------------------------
if (d['status'].toString() != 'yet') {
// استخراج التوكن من الرد المجمع (دون طلب جديد)
List? tokenList = jsonResponse['driver_token'];
// توليد بصمة الجهاز الحالية
String currentFingerPrint =
await DeviceHelper.getDeviceFingerprint();
await storage.write(
key: BoxName.fingerPrint, value: currentFingerPrint);
if (tokenList != null && tokenList.isNotEmpty) {
var tokenData = tokenList[0];
String serverToken = tokenData['token'].toString();
String serverFingerPrint = tokenData['fingerPrint'].toString();
String localToken =
box.read(BoxName.tokenDriver)?.toString() ?? "";
// التحقق من تطابق التوكن والبصمة
if (serverToken != localToken ||
serverFingerPrint != currentFingerPrint) {
isloading = false;
update();
await Get.defaultDialog(
barrierDismissible: false,
title: 'Device Change Detected'.tr,
middleText: 'Please verify your identity'.tr,
textConfirm: 'Verify'.tr,
confirmTextColor: Colors.white,
onConfirm: () {
Get.back(); // إغلاق الديالوج
// الانتقال لصفحة التحقق مع تمرير البيانات اللازمة
Get.to(() => OtpVerificationPage(
phone: d['phone'].toString(),
deviceToken: currentFingerPrint,
// نمرر التوكن القادم من السيرفر ليستعمل في التحقق لاحقاً
token: jsonEncode(
{'status': 'success', 'data': tokenList}),
ptoken: serverToken,
));
},
);
return true; // إيقاف التنفيذ هنا (بانتظار التحقق)
}
}
// إذا كل شيء سليم، الدخول للرئيسية
Get.offAll(() => HomeCaptain());
} else {
// الحالة "yet" تعني أن السائق لم يكمل رفع الوثائق
Get.offAll(() => DriverVerificationScreen());
}
isloading = false;
update();
return true;
} else {
// الحساب موجود لكن رقم الهاتف غير مفعل
Get.offAll(() => PhoneNumberScreen());
isloading = false;
update();
return false;
}
} else {
// الرد من السيرفر ليس success (رغم أنه ليس failure في الاتصال)
Get.offAll(() => PhoneNumberScreen());
isloading = false;
update();
return false;
}
}
} catch (e) {
Log.print("Login Critical Error: $e");
isloading = false;
update();
return false;
}
}
logintest(String driverID, email) async {
isloading = true;
update();
// await SecurityHelper.performSecurityChecks();
// Log.print('(BoxName.emailDriver): ${box.read(BoxName.emailDriver)}');
var res = await CRUD()
.get(link: AppLink.loginUsingCredentialsWithoutGoogle, payload: {
'email': email ?? 'yet',
'id': driverID,
});
// 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()) > 2017) {
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()) < 2017) {
box.write(BoxName.carTypeOfDriver, 'Speed');
} else if (int.parse(d['year'].toString()) < 2002) {
box.write(BoxName.carTypeOfDriver, 'Awfar Car');
}
// 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 {
// mySnackbarSuccess('');
// isloading = false;
// update();
}
}
}
loginUsingCredentialsWithoutGoogle(
String password, String email, String phone) async {
isloading = true;
// تحديد نوع العملية (ليس جوجل)
isGoogleLogin = false;
update();
try {
String platformName = Platform.isIOS ? 'ios' : 'android';
// 1. إرسال الطلب (إضافة Platform)
var res = await CRUD()
.get(link: AppLink.loginUsingCredentialsWithoutGoogle, payload: {
'email': email,
'password': password,
'phone': phone, // <-- يجب إضافة هذا السطر
'platform': platformName
});
// حفظ الإيميل لمحاولات الدخول اللاحقة
box.write(BoxName.emailDriver, email);
if (res == 'failure') {
// في حال الفشل (كلمة مرور خطأ أو مستخدم غير موجود)
// نقوم بفحص حالة الهاتف إذا كان مسجلاً من قبل لتوجيهه
await isPhoneVerified();
isloading = false;
update();
return false;
} else {
var jsonResponse = jsonDecode(res);
if (jsonResponse['status'] == 'success' &&
jsonResponse['data'].isNotEmpty) {
var d = jsonResponse['data'][0];
// 2. التحقق من تفعيل الهاتف
if (d['is_verified'].toString() == '1') {
// --- A. حفظ البيانات الأساسية ---
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.nameArabic, d['name_arabic']);
box.write(BoxName.bankCodeDriver, d['bankCode']);
box.write(BoxName.accountBankNumberDriver, d['accountBank']);
box.write(
BoxName.nameDriver, '${d['first_name']} ${d['last_name']}');
// --- B. منطق نوع السيارة ---
int categoryId =
int.tryParse(d['vehicle_category_id']?.toString() ?? '0') ?? 0;
int carYear = int.tryParse(d['year']?.toString() ?? '0') ?? 0;
box.write(BoxName.carYear, carYear);
String gender = d['gender']?.toString() ?? '';
if (categoryId == 2) {
if (gender == 'Male') {
box.write(BoxName.carTypeOfDriver, 'Scooter');
} else {
box.write(BoxName.carTypeOfDriver, 'Pink Bike');
}
} else if (categoryId == 3) {
box.write(BoxName.carTypeOfDriver, 'Van');
} else if (carYear > 2017) {
if (gender != 'Male') {
box.write(BoxName.carTypeOfDriver, 'Lady');
} else {
box.write(BoxName.carTypeOfDriver, 'Comfort');
}
} else if (carYear > 2002 && carYear <= 2017) {
box.write(BoxName.carTypeOfDriver, 'Speed');
} else if (carYear <= 2002 && carYear > 0) {
box.write(BoxName.carTypeOfDriver, 'Awfar Car');
}
// --- C. كود الدعوة ---
if (box.read(BoxName.isInstall) == null ||
box.read(BoxName.isInstall).toString() == '0') {
updateInvitationCodeFromRegister();
}
// --- D. التحقق الأمني (Device Fingerprint) ---
if (d['status'].toString() != 'yet') {
// الـ PHP المعدل يعيد driver_token داخل object السائق
List? tokenList = d['driver_token'];
String currentFingerPrint =
await DeviceHelper.getDeviceFingerprint();
await storage.write(
key: BoxName.fingerPrint, value: currentFingerPrint);
if (tokenList != null && tokenList.isNotEmpty) {
// نأخذ أحدث توكن (أو الأول في القائمة)
var tokenData = tokenList[0];
String serverToken = tokenData['token'].toString();
String serverFingerPrint = tokenData['fingerPrint'].toString();
String localToken =
box.read(BoxName.tokenDriver)?.toString() ?? "";
// إذا اختلف التوكن أو البصمة => جهاز جديد
if (serverToken != localToken ||
serverFingerPrint != currentFingerPrint) {
isloading = false;
update();
await Get.defaultDialog(
barrierDismissible: false,
title: 'Device Change Detected'.tr,
middleText: 'Please verify your identity'.tr,
textConfirm: 'Verify'.tr,
confirmTextColor: Colors.white,
onConfirm: () {
Get.back();
Get.to(() => OtpVerificationPage(
phone: d['phone'].toString(),
deviceToken: currentFingerPrint,
// تمرير قائمة التوكنات لاستخدامها في التحقق
token: jsonEncode(
{'status': 'success', 'data': tokenList}),
ptoken: serverToken,
));
},
);
return true;
}
}
// الدخول للرئيسية
Get.offAll(() => HomeCaptain());
} else {
// لم يكمل الوثائق
Get.offAll(() => DriverVerificationScreen());
}
isloading = false;
update();
return true;
} else {
// الحساب موجود لكن غير مفعل
Get.offAll(() => PhoneNumberScreen());
isloading = false;
update();
return false;
}
} else {
// استجابة غير متوقعة أو data فارغة
Get.snackbar("Error".tr, "Login Failed".tr);
isloading = false;
update();
return false;
}
}
} catch (e) {
Log.print("Login Credentials Error: $e");
isloading = false;
update();
return false;
}
}
void loginByBoxData() async {
Get.to(() => HomeCaptain());
await CRUD().post(link: AppLink.addTokensDriver, payload: {
'token': box.read(BoxName.tokenDriver).toString(),
'captain_id': box.read(BoxName.driverID).toString()
});
}
}
@@ -1,93 +0,0 @@
import 'dart:io';
import 'package:get/get.dart';
// import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';
import 'package:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart';
import 'package:sefer_driver/constant/colors.dart';
import 'package:sefer_driver/controller/functions/llama_ai.dart';
// class CarRegistrationRecognizerController extends GetxController {
// @override
// void onInit() {
// // scanText();
// super.onInit();
// }
// // The ImagePicker instance
// final ImagePicker _imagePicker = ImagePicker();
// // The GoogleMlKit TextRecognizer instance
// // final TextRecognizer _textRecognizer = TextRecognizer();
// // The scanned text
// String? scannedText;
// String? jsonOutput;
// final List<Map<String, dynamic>> lines = [];
// Map extracted = {};
// XFile? image;
// CroppedFile? croppedFile;
// // Picks an image from the camera or gallery and extracts the text
// final List<Map<String, dynamic>> extractedTextWithCoordinates = [];
// Future<void> scanText() async {
// // Pick an image from the camera or gallery
// image = await _imagePicker.pickImage(source: ImageSource.gallery);
// update();
// // If no image was picked, return
// if (image == null) {
// return;
// }
// // Crop the image
// croppedFile = await ImageCropper().cropImage(
// sourcePath: image!.path,
// //
// uiSettings: [
// AndroidUiSettings(
// toolbarTitle: 'Cropper'.tr,
// toolbarColor: AppColor.blueColor,
// toolbarWidgetColor: AppColor.yellowColor,
// initAspectRatio: CropAspectRatioPreset.original,
// lockAspectRatio: false),
// IOSUiSettings(
// title: 'Cropper'.tr,
// ),
// ],
// );
// // If no cropped image was obtained, return
// if (croppedFile == null) {
// return;
// }
// // Convert the cropped file to an InputImage object
// final InputImage inputImage = InputImage.fromFile(File(croppedFile!.path));
// // Recognize the text in the image
// final RecognizedText recognizedText =
// await _textRecognizer.processImage(inputImage);
// scannedText = recognizedText.text;
// // Extract the scanned text line by line
// final List<Map<String, dynamic>> lines = [];
// for (var i = 0; i < recognizedText.blocks.length; i++) {
// lines.add({
// i.toString(): recognizedText.blocks[i].text,
// });
// }
// String result = lines.map((map) => map.values.first.toString()).join(' ');
// if (result.length > 2200) {
// result = result.substring(0, 2200);
// }
// Map result2 = await LlamaAi().getCarRegistrationData(result,
// 'vin,make,made,year,expiration_date,color,owner,registration_date'); //
// // Assign the result to the extracted variable
// extracted = result2;
// update();
// }
// }
@@ -1,114 +0,0 @@
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';
import '../../../constant/links.dart';
import '../../../main.dart';
import '../../firebase/firbase_messge.dart';
import '../../firebase/notification_service.dart';
import '../../functions/crud.dart';
class OtpVerificationController extends GetxController {
final String phone;
final String deviceToken;
final String token;
final otpCode = ''.obs;
final isLoading = false.obs;
final isVerifying = false.obs;
var canResend = false.obs;
var countdown = 120.obs;
Timer? _timer;
OtpVerificationController({
required this.phone,
required this.deviceToken,
required this.token,
});
@override
void onInit() {
super.onInit();
sendOtp(); // ترسل تلقائيًا عند فتح الصفحة
startCountdown();
}
void startCountdown() {
canResend.value = false;
countdown.value = 120;
_timer?.cancel();
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (countdown.value > 0) {
countdown.value--;
} else {
canResend.value = true;
timer.cancel();
}
});
}
Future<void> sendOtp() async {
isLoading.value = true;
try {
final response = await CRUD().post(
link:
'${AppLink.server}/auth/token_passenger/driver/send_otp_driver.php',
payload: {
'receiver': phone,
// 'device_token': deviceToken,
},
);
if (response != 'failure') {
// بإمكانك عرض رسالة نجاح هنا
} else {
// Get.snackbar('Error', 'Failed to send OTP');
}
} catch (e) {
Get.snackbar('Error', e.toString());
} finally {
isLoading.value = false;
}
}
Future<void> verifyOtp(String ptoken) async {
isVerifying.value = true;
var finger = await storage.read(key: BoxName.fingerPrint);
try {
final response = await CRUD().post(
link:
'${AppLink.server}/auth/token_passenger/driver/verify_otp_driver.php',
payload: {
'phone_number': phone,
'otp': otpCode.value,
'token': box.read(BoxName.tokenDriver).toString(),
'fingerPrint': finger.toString(),
},
);
if (response != 'failure') {
Log.print('response: ${response}');
// Get.back(); // توجه إلى الصفحة التالية
await NotificationService.sendNotification(
target: ptoken.toString(),
title: 'token change'.tr,
body: 'token change'.tr,
isTopic: false, // Important: this is a token
tone: 'cancel',
driverList: [], category: 'token change',
);
Get.offAll(() => HomeCaptain());
} else {
Get.snackbar('Verification Failed', 'OTP is incorrect or expired');
}
} catch (e) {
Get.snackbar('Error', e.toString());
} finally {
isVerifying.value = false;
}
}
}
@@ -1,161 +0,0 @@
import 'package:get/get.dart';
import 'package:sefer_driver/controller/auth/captin/login_captin_controller.dart';
import 'package:sefer_driver/controller/functions/crud.dart';
import 'package:sefer_driver/print.dart';
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
import '../../../constant/box_name.dart';
import '../../../constant/links.dart';
import '../../../main.dart';
// تأكد من تعديل هذا المسار لصفحة التسجيل الحالية
class PhoneAuthHelper {
// --- New Endpoints for Egypt Market ---
// تم تحديث المسارات بناءً على طلبك لتعمل مع ملفات الكابتن الجديدة
static final String _baseUrl = '${AppLink.server}/auth/captin/';
static final String _sendOtpUrl = '${_baseUrl}sendOtpMessageDriver.php';
static final String _verifyOtpUrl = '${_baseUrl}verifyOtpDriver.php';
/// دالة تنسيق الرقم للسوق المصري والعالمي
static String formatPhoneNumber(String phone) {
// 1. إزالة المسافات والرموز الزائدة
String cleanPhone = phone.replaceAll(RegExp(r'[ \-\(\)]'), '').trim();
// 2. التحقق إذا كان الرقم مصرياً (يبدأ بـ 010, 011, 012, 015 أو +20)
// إذا بدأ بـ +20، نتركه كما هو (صيغة دولية صحيحة)
if (cleanPhone.startsWith('+20')) {
return cleanPhone;
}
// إذا بدأ بـ 01 (مثل 010xxxx...)، نحذف الصفر ونضيف +20
if (cleanPhone.startsWith('01')) {
return '+20${cleanPhone.substring(1)}';
}
// إذا بدأ بـ 1 مباشرة (مثل 10xxxx...) وكان الطول 10 أرقام، نضيف +20
if (cleanPhone.startsWith('1') && cleanPhone.length == 10) {
return '+20$cleanPhone';
}
// 3. للأرقام الدولية الأخرى (غير مصر):
// يجب التأكد من وجود علامة + في البداية ليعرف السيرفر أنه رقم دولي (للواتساب)
if (!cleanPhone.startsWith('+')) {
return '+$cleanPhone';
}
return cleanPhone;
}
/// إرسال الرمز (SMS للمصريين، واتساب للأجانب - يتم التعامل معه في السيرفر)
static Future<bool> sendOtp(String phoneNumber, {String? email}) async {
try {
final fixedPhone = formatPhoneNumber(phoneNumber);
Log.print('Sending OTP to: $fixedPhone');
// نجهز الإيميل والآيدي الحاليين
String currentDriverId = box.read(BoxName.driverID)?.toString() ?? '';
String currentEmail =
email ?? box.read(BoxName.emailDriver)?.toString() ?? '';
// إرسال الطلب للسيرفر
final response = await CRUD().post(
link: _sendOtpUrl,
payload: {
'phone_number': fixedPhone,
'driverId': currentDriverId,
'email': currentEmail,
},
);
Log.print('Send OTP Response: $response');
if (response != 'failure') {
// فك الترميز في حال كان الرد نصاً
final data = response;
if (data['status'] == 'success') {
// -----------------------------------------------------------
// هام جداً: حفظ البيانات التي ولدها السيرفر (ID & Email)
// -----------------------------------------------------------
if (data['driverId'] != null &&
data['driverId'].toString().isNotEmpty) {
String newId = data['driverId'].toString();
Log.print("New Driver ID Generated by Server: $newId");
box.write(BoxName.driverID, newId);
}
if (data['email'] != null && data['email'].toString().isNotEmpty) {
String newEmail = data['email'].toString();
Log.print("New Email Generated by Server: $newEmail");
box.write(BoxName.emailDriver, newEmail);
}
mySnackbarSuccess('Verification code sent successfully'.tr);
return true;
} else {
mySnackeBarError(data['message'] ?? 'Failed to send code'.tr);
return false;
}
} else {
mySnackeBarError('Server Error, please try again'.tr);
return false;
}
} catch (e) {
Log.print('Error sending OTP: $e');
return false;
}
}
/// التحقق من الرمز المدخل
/// ملاحظة هامة: دالة PHP الجديدة للتحقق لا ترجع بيانات السائق، فقط تقول "تم التحقق".
/// لذا نحتاج لمنطق إضافي هنا لتسجيل الدخول أو التسجيل.
static Future<void> verifyOtp(String phoneNumber, [String? otpCode]) async {
try {
if (otpCode == null || otpCode.isEmpty) {
mySnackeBarError('يرجى إدخال الكود'.tr);
return;
}
final fixedPhone = formatPhoneNumber(phoneNumber);
Log.print('Verifying OTP for: $fixedPhone with code: $otpCode');
final response = await CRUD().post(
link: _verifyOtpUrl,
payload: {
'phone_number': fixedPhone,
'token_code': otpCode, // ملف PHP الجديد يطلب token_code
},
);
if (response != 'failure') {
final data = response;
if (data['status'] == 'success') {
// 1. التحقق تم بنجاح في قاعدة بيانات الهاتف
// بما أن ملف verifyOtpDriver.php الجديد لا يرجع بيانات السائق،
// يجب علينا الآن محاولة تسجيل الدخول بهذا الرقم لمعرفة هل هو مسجل أم لا.
box.write(BoxName.firstTimeLoadKey, 'true');
box.write(BoxName.phoneDriver, fixedPhone);
var driverId = box.read(BoxName.driverID);
Log.print('driverId: ${driverId}');
var email = box.read(BoxName.emailDriver);
Log.print('email: ${email}');
await Get.put(LoginDriverController()).loginWithGoogleCredential(
driverId,
email,
);
} else {
mySnackeBarError(data['message'] ?? 'كود التحقق غير صحيح'.tr);
}
} else {
mySnackeBarError('خطأ في الاتصال بالسيرفر'.tr);
}
} catch (e) {
mySnackeBarError('حدث خطأ: $e');
}
}
/// دالة مساعدة لمحاولة تسجيل الدخول أو التوجيه للتسجيل
/// هذه الدالة ضرورية لأن ملف التحقق الجديد فصل عملية التحقق عن جلب البيانات
static Future<void> _checkUserAndLogin() async {}
}
@@ -1,419 +0,0 @@
import 'dart:convert';
import 'dart:math';
import 'package:sefer_driver/controller/auth/captin/login_captin_controller.dart';
import 'package:sefer_driver/views/auth/captin/egypt_card_a_i.dart';
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
import 'package:flutter/material.dart';
import 'package:get/get.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/functions/ocr_controller.dart';
import 'package:sefer_driver/main.dart';
import 'package:sefer_driver/views/auth/captin/login_captin.dart';
import 'package:sefer_driver/views/auth/captin/verify_email_captain.dart';
import '../../../constant/colors.dart';
import '../../../views/auth/captin/ai_eg_controller.dart';
import '../../../views/auth/syria/registration_view.dart';
import '../../../views/home/Captin/home_captain/home_captin.dart';
import '../../functions/sms_egypt_controller.dart';
class RegisterCaptainController extends GetxController {
final formKey = GlobalKey<FormState>();
final formKey3 = GlobalKey<FormState>();
TextEditingController emailController = TextEditingController();
TextEditingController phoneController = TextEditingController();
TextEditingController passwordController = TextEditingController();
TextEditingController verifyCode = TextEditingController();
String birthDate = 'Birth Date'.tr;
String gender = 'Male'.tr;
bool isLoading = false;
bool isSent = false;
late String name;
late String licenseClass;
late String documentNo;
late String address;
late String height;
late String postalCode;
late String sex;
late String stateCode;
late String expireDate;
late String dob;
getBirthDate() {
Get.defaultDialog(
title: 'Select Date'.tr,
content: SizedBox(
width: 300,
child: CalendarDatePicker(
initialDate: DateTime.now().subtract(const Duration(days: 18 * 365)),
firstDate: DateTime.parse('1940-06-01'),
lastDate: DateTime.now().subtract(const Duration(days: 18 * 365)),
onDateChanged: (date) {
// Get the selected date and convert it to a DateTime object
DateTime dateTime = date;
// Call the getOrders() function from the controller
birthDate = dateTime.toString().split(' ')[0];
update();
Get.back();
},
// onDateChanged: (DateTime value) {},
),
),
);
}
@override
void onInit() {
// Get.put(SmsEgyptController());
super.onInit();
}
void changeGender(String value) {
gender = value;
update();
}
bool isValidEgyptianPhoneNumber(String phoneNumber) {
// Remove any non-digit characters (spaces, dashes, etc.)
phoneNumber = phoneNumber.replaceAll(RegExp(r'\D+'), '');
// Check if the phone number has exactly 11 digits
if (phoneNumber.length != 11) {
return false;
}
// Check if the phone number starts with 010, 011, 012, or 015
RegExp validPrefixes = RegExp(r'^01[0125]\d{8}$');
return validPrefixes.hasMatch(phoneNumber);
}
sendOtpMessage() async {
SmsEgyptController smsEgyptController = Get.put(SmsEgyptController());
isLoading = true;
update();
isLoading = true;
update();
if (formKey3.currentState!.validate()) {
if (box.read(BoxName.countryCode) == 'Egypt') {
if (isValidEgyptianPhoneNumber(phoneController.text)) {
var responseCheker = await CRUD()
.post(link: AppLink.checkPhoneNumberISVerfiedDriver, payload: {
'phone_number': ('+2${phoneController.text}'),
});
if (responseCheker != 'failure') {
var d = jsonDecode(responseCheker);
if (d['message'][0]['is_verified'].toString() == '1') {
Get.snackbar('Phone number is verified before'.tr, '',
backgroundColor: AppColor.greenColor);
box.write(BoxName.phoneVerified, '1');
box.write(BoxName.phone, ('+2${phoneController.text}'));
await Get.put(LoginDriverController()).loginWithGoogleCredential(
box.read(BoxName.driverID).toString(),
(box.read(BoxName.emailDriver).toString()),
);
} else {
await CRUD().post(link: AppLink.sendVerifyOtpMessage, payload: {
'phone_number': ('+2${phoneController.text}'),
"driverId": box.read(BoxName.driverID),
"email": (box.read(BoxName.emailDriver)),
});
isSent = true;
isLoading = false;
update();
}
} else {
await CRUD().post(link: AppLink.sendVerifyOtpMessage, payload: {
'phone_number': ('+2${phoneController.text}'),
"driverId": box.read(BoxName.driverID),
"email": box.read(BoxName.emailDriver),
});
isSent = true;
isLoading = false;
update();
}
} else {
mySnackeBarError(
'Phone Number wrong'.tr,
);
}
}
}
isLoading = false;
update();
}
DateTime? lastOtpSentTime; // Store the last OTP sent time
int otpResendInterval = 300; // 5 minutes in seconds
// Main function to handle OTP sending
// sendOtpMessage() async {
// if (_isOtpResendAllowed()) {
// isLoading = true;
// update();
// if (formKey3.currentState!.validate()) {
// String countryCode = box.read(BoxName.countryCode);
// String phoneNumber = phoneController.text;
// if (countryCode == 'Egypt' && isValidEgyptianPhoneNumber(phoneNumber)) {
// await _checkAndSendOtp(phoneNumber);
// } else {
// _showErrorMessage('Phone Number is not Egypt phone '.tr);
// }
// }
// isLoading = false;
// update();
// } else {
// _showCooldownMessage();
// }
// }
// Check if the resend OTP request is allowed (5 minutes cooldown)
// bool _isOtpResendAllowed() {
// if (lastOtpSentTime == null) return true;
// final int elapsedTime =
// DateTime.now().difference(lastOtpSentTime!).inSeconds;
// return elapsedTime >= otpResendInterval;
// }
// // Show message when user tries to resend OTP too soon
// void _showCooldownMessage() {
// int remainingTime = otpResendInterval -
// DateTime.now().difference(lastOtpSentTime!).inSeconds;
// Get.snackbar(
// 'Please wait ${remainingTime ~/ 60}:${(remainingTime % 60).toString().padLeft(2, '0')} minutes before requesting again',
// '',
// backgroundColor: AppColor.redColor,
// );
// }
// // Check if the phone number has been verified, and send OTP if not verified
// _checkAndSendOtp(String phoneNumber) async {
// var responseChecker = await CRUD().post(
// link: AppLink.checkPhoneNumberISVerfiedDriver,
// payload: {
// 'phone_number': '+2$phoneNumber',
// },
// );
// if (responseChecker != 'failure') {
// var responseData = jsonDecode(responseChecker);
// if (_isPhoneVerified(responseData)) {
// _handleAlreadyVerified();
// } else {
// await _sendOtpAndSms(phoneNumber);
// }
// } else {
// await _sendOtpAndSms(phoneNumber);
// }
// }
// Check if the phone number is already verified
bool _isPhoneVerified(dynamic responseData) {
return responseData['message'][0]['is_verified'].toString() == '1';
}
// Handle case where phone number is already verified
_handleAlreadyVerified() {
mySnackbarSuccess('Phone number is already verified'.tr);
box.write(BoxName.phoneVerified, '1');
box.write(BoxName.phone, ('+2${phoneController.text}'));
Get.put(LoginDriverController()).loginWithGoogleCredential(
box.read(BoxName.driverID).toString(),
box.read(BoxName.emailDriver).toString(),
);
}
// Send OTP and SMS
_sendOtpAndSms(String phoneNumber) async {
SmsEgyptController smsEgyptController = Get.put(SmsEgyptController());
int randomNumber = Random().nextInt(100000) + 1;
await CRUD().post(
link: AppLink.sendVerifyOtpMessage,
payload: {
'phone_number': ('+2$phoneNumber'),
'token_code': (randomNumber.toString()),
'driverId': box.read(BoxName.driverID),
'email': box.read(BoxName.emailDriver),
},
);
await smsEgyptController.sendSmsEgypt(phoneNumber);
lastOtpSentTime = DateTime.now(); // Update the last OTP sent time
isSent = true;
isLoading = false;
update();
}
verifySMSCode() async {
// var loginDriverController = Get.put(LoginDriverController());
if (formKey3.currentState!.validate()) {
var res = await CRUD().post(link: AppLink.verifyOtpDriver, payload: {
'phone_number': ('+2${phoneController.text}'),
'token_code': (verifyCode.text.toString()),
});
if (res != 'failure') {
// var dec = jsonDecode(res);
box.write(BoxName.phoneDriver, ('+2${phoneController.text}'));
box.write(BoxName.phoneVerified, '1');
// loginDriverController.isGoogleLogin == true
// ? await loginDriverController.loginUsingCredentialsWithoutGoogle(
// loginDriverController.passwordController.text.toString(),
// box.read(BoxName.emailDriver).toString(),
// )
// : await loginDriverController.loginUsingCredentials(
// box.read(BoxName.driverID).toString(),
// box.read(BoxName.emailDriver).toString(),
// );
// Get.offAll(() => SyrianCardAI());
Get.to(() => RegistrationView());
// } else {
// Get.snackbar('title', 'message');
// }
}
} else {
mySnackeBarError('you must insert token code '.tr);
}
}
sendVerifications() async {
var res = await CRUD().post(link: AppLink.verifyEmail, payload: {
'email': emailController.text.isEmpty
? (Get.find<LoginDriverController>().emailController.text.toString())
: (emailController.text),
'token': (verifyCode.text),
});
if (res != 'failure') {
if (Get.find<LoginDriverController>().emailController.text.toString() !=
'') {
Get.offAll(() => HomeCaptain());
} else {
// Get.to(() => CarLicensePage());
}
}
}
void nextToAIDetection() async {
//Todo dont forget this
if (formKey.currentState!.validate()) {
isLoading = true;
update();
Get.to(() => EgyptCardAI());
}
}
Map<String, dynamic> payloadLisence = {};
void getFromController() {
name = Get.find<ScanDocumentsByApi>().name;
licenseClass = Get.find<ScanDocumentsByApi>().licenseClass.toString();
documentNo = Get.find<ScanDocumentsByApi>().documentNo.toString();
address = Get.find<ScanDocumentsByApi>().address.toString();
height = Get.find<ScanDocumentsByApi>().height.toString();
postalCode = Get.find<ScanDocumentsByApi>().address.toString();
sex = Get.find<ScanDocumentsByApi>().sex.toString();
stateCode = Get.find<ScanDocumentsByApi>().postalCode.toString();
expireDate = Get.find<ScanDocumentsByApi>().expireDate.toString();
dob = Get.find<ScanDocumentsByApi>().dob.toString();
update();
}
Future addLisence() async {
getFromController();
var res = await CRUD().post(link: AppLink.addLicense, payload: {
'name': name,
'licenseClass': licenseClass,
'documentNo': documentNo,
'address': address,
'height': height,
'postalCode': postalCode,
'sex': sex,
'stateCode': stateCode,
'expireDate': expireDate,
'dateOfBirth': dob,
});
isLoading = false;
update();
if (jsonDecode(res)['status'] == 'success') {
// Get.to(() => AiPage()); //todo rplace this
}
}
void addRegisrationCarForDriver(String vin, make, model, year, color, owner,
expirationDate, registrationDate) async {
getFromController();
var res = await CRUD().post(link: AppLink.addRegisrationCar, payload: {
'vin': vin,
'make': make,
'model': model,
'year': year,
'expirationDate': expirationDate,
'color': color,
'owner': owner,
'registrationDate': registrationDate,
});
box.write(BoxName.vin, vin);
box.write(BoxName.make, make);
box.write(BoxName.model, model);
box.write(BoxName.year, year);
box.write(BoxName.expirationDate, expirationDate);
box.write(BoxName.color, color);
box.write(BoxName.owner, owner);
box.write(BoxName.registrationDate, registrationDate);
isLoading = false;
update();
if (jsonDecode(res)['status'] == 'success') {
Get.offAll(() => LoginCaptin()); //todo replace this
}
}
Future register() async {
getFromController();
if (formKey.currentState!.validate()) {
isLoading = true;
update();
var res = await CRUD().post(link: AppLink.signUpCaptin, payload: {
'first_name': name.split(' ')[1],
'last_name': name.split(' ')[0],
'email': emailController.text,
'phone': phoneController.text,
'password': passwordController.text,
'gender': sex,
'site': address,
'birthdate': dob,
});
isLoading = false;
update();
if (jsonDecode(res)['status'] == 'success') {
box.write(BoxName.driverID, jsonDecode(res)['message']);
box.write(BoxName.dobDriver, dob);
box.write(BoxName.sexDriver, sex);
box.write(BoxName.phoneDriver, phoneController.text);
box.write(BoxName.lastNameDriver, name.split(' ')[0]);
int randomNumber = Random().nextInt(100000) + 1;
await CRUD().post(link: AppLink.sendVerifyEmail, payload: {
'email': emailController.text,
'token': randomNumber.toString(),
});
Get.to(() => VerifyEmailCaptainPage());
}
}
}
}