feat: استيراد كود سيرو إلى تريبز (سيرو @ecfe7568) — بلا تعديل
قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ. الخريطة: backend · payment_server · loction_server · ride_server · passenger_server · docker · dashboard · stress_test → الجذر siro_rider → apps/rider siro_driver → apps/driver siro_admin → dashboards/admin siro_service → dashboards/service android_bot → apps/android_bot socialBot → apps/socialBot نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب) لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً: كل ما يلي يصير فرقاً مقروءاً مقابل المصدر. لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز، سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh (ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و dashboards/transit-web). ⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة: 1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر): كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner. 2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist) يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً. 3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع → يجب ضمّ الحزم داخله أسوة بـ apps/rider. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9909d9b4c1
commit
4d8414c96b
@@ -0,0 +1,60 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:siro_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';
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,570 @@
|
||||
import 'package:siro_driver/constant/currency.dart';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
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:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/controller/home/payment/captain_wallet_controller.dart';
|
||||
import 'package:siro_driver/print.dart';
|
||||
import 'package:siro_driver/controller/functions/country_logic.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:siro_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 Siro as a driver using my referral code!
|
||||
Use code: $driverCouponCode
|
||||
Download the Siro Driver app now and earn rewards:
|
||||
https://siromove.com/invite.php?code=$driverCouponCode&app=driver
|
||||
''';
|
||||
await Share.share(shareText);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sharePassengerCode() async {
|
||||
if (couponCode != null) {
|
||||
final String shareText = '''Get a discount on your first Siro ride!
|
||||
Use my referral code: $couponCode
|
||||
Download the Siro app now and enjoy your ride:
|
||||
https://siromove.com/invite.php?code=$couponCode&app=rider
|
||||
''';
|
||||
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) {
|
||||
// mySnackbarError('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 {
|
||||
log('=== START: FETCHING ALL CONTACTS FOR BOTTOM SHEET ===',
|
||||
name: 'ContactPicker');
|
||||
|
||||
if (await FlutterContacts.requestPermission(readonly: true)) {
|
||||
// عرض شاشة تحميل بسيطة ريثما يتم جلب الأسماء
|
||||
Get.dialog(const Center(child: CircularProgressIndicator()),
|
||||
barrierDismissible: false);
|
||||
|
||||
log('Permission granted. Calling FlutterContacts.getContacts()...',
|
||||
name: 'ContactPicker');
|
||||
|
||||
// جلب جميع جهات الاتصال إجبارياً من الصفر مع خصائصها
|
||||
List<Contact> allContacts =
|
||||
await FlutterContacts.getContacts(withProperties: true);
|
||||
|
||||
log('Total Contacts Fetched from Device: ${allContacts.length}',
|
||||
name: 'ContactPicker');
|
||||
|
||||
// فصل الأسماء لمعرفة الخلل
|
||||
List<Contact> validContacts = [];
|
||||
List<Contact> invalidContacts = [];
|
||||
|
||||
for (var c in allContacts) {
|
||||
if (c.phones.isNotEmpty) {
|
||||
validContacts.add(c);
|
||||
} else {
|
||||
invalidContacts.add(c);
|
||||
}
|
||||
}
|
||||
|
||||
log('Contacts WITH phone numbers: ${validContacts.length}',
|
||||
name: 'ContactPicker');
|
||||
log('Contacts WITHOUT phone numbers: ${invalidContacts.length}',
|
||||
name: 'ContactPicker');
|
||||
|
||||
// طباعة أول 20 اسم صالح
|
||||
log('--- Sample of VALID Contacts ---', name: 'ContactPicker');
|
||||
for (int i = 0; i < validContacts.length && i < 20; i++) {
|
||||
log('[$i] Name: ${validContacts[i].displayName}, Phone: ${validContacts[i].phones.first.number}',
|
||||
name: 'ContactPicker');
|
||||
}
|
||||
|
||||
// طباعة أول 20 اسم غير صالح (بدون أرقام) لفحص المشكلة
|
||||
log('--- Sample of INVALID Contacts (No Phone) ---',
|
||||
name: 'ContactPicker');
|
||||
for (int i = 0; i < invalidContacts.length && i < 20; i++) {
|
||||
log('[$i] Name: ${invalidContacts[i].displayName}',
|
||||
name: 'ContactPicker');
|
||||
}
|
||||
|
||||
Get.back(); // إغلاق شاشة التحميل
|
||||
|
||||
if (validContacts.isEmpty) {
|
||||
mySnackbarError('No contacts with phone numbers found'.tr);
|
||||
return;
|
||||
}
|
||||
|
||||
// متغيرات للبحث داخل القائمة المنسدلة
|
||||
RxList<Contact> filteredContacts = validContacts.obs;
|
||||
TextEditingController searchController = TextEditingController();
|
||||
|
||||
// دالة لتنظيف النصوص من أي رموز معطوبة
|
||||
String sanitizeText(String input) {
|
||||
if (input.isEmpty) return '';
|
||||
return input
|
||||
.replaceAll(
|
||||
RegExp(r'[^\x00-\x7F\u0600-\u06FF\u08A0-\u08FF\p{L}\p{N}\s]'),
|
||||
'')
|
||||
.trim();
|
||||
}
|
||||
|
||||
// فتح دليل هاتف مخصص داخل التطبيق
|
||||
Get.bottomSheet(
|
||||
Container(
|
||||
height: Get.height * 0.85,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(Get.context!).scaffoldBackgroundColor,
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 10, bottom: 10),
|
||||
width: 50,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[400],
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
Text("Select a Contact".tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search name or number...".tr,
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 0),
|
||||
),
|
||||
onChanged: (value) {
|
||||
filteredContacts.value = validContacts.where((c) {
|
||||
final nameMatch = c.displayName
|
||||
.toLowerCase()
|
||||
.contains(value.toLowerCase());
|
||||
final phoneMatch =
|
||||
c.phones.first.number.contains(value);
|
||||
return nameMatch || phoneMatch;
|
||||
}).toList();
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Obx(() => ListView.builder(
|
||||
itemCount: filteredContacts.length,
|
||||
itemBuilder: (context, index) {
|
||||
Contact c = filteredContacts[index];
|
||||
var firstPhone = c.phones.first;
|
||||
|
||||
String selectedPhone =
|
||||
firstPhone.normalizedNumber.isNotEmpty
|
||||
? firstPhone.normalizedNumber
|
||||
: firstPhone.number;
|
||||
|
||||
String safeName = sanitizeText(c.displayName);
|
||||
if (safeName.isEmpty) safeName = 'Unknown'.tr;
|
||||
|
||||
String safePhone = sanitizeText(selectedPhone);
|
||||
String initial = safeName.isNotEmpty
|
||||
? safeName[0].toUpperCase()
|
||||
: '?';
|
||||
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Colors.blueAccent.withOpacity(0.1),
|
||||
child: Text(initial,
|
||||
style: const TextStyle(
|
||||
color: Colors.blueAccent)),
|
||||
),
|
||||
title: Text(safeName),
|
||||
subtitle: Text(safePhone,
|
||||
textDirection: TextDirection.ltr),
|
||||
onTap: () {
|
||||
selectPhone(selectedPhone);
|
||||
},
|
||||
);
|
||||
},
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
isScrollControlled: true,
|
||||
);
|
||||
} else {
|
||||
log('Permission DENIED', name: 'ContactPicker');
|
||||
mySnackbarError('Contact permission is required to pick contacts'.tr);
|
||||
}
|
||||
} catch (e) {
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
log('CRITICAL ERROR: $e', name: 'ContactPicker');
|
||||
mySnackbarError('An error occurred while loading contacts: $e'.tr);
|
||||
}
|
||||
log('=== END: FETCHING CONTACTS ===', name: 'ContactPicker');
|
||||
}
|
||||
|
||||
/// **FIX**: Added the missing 'selectPhone' method.
|
||||
void selectPhone(String phone) {
|
||||
// Format the selected phone number and update the text field.
|
||||
invitePhoneController.text = CountryLogic.formatCurrentCountryPhone(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();
|
||||
// Server-side claim validation
|
||||
var response = await CRUD().post(
|
||||
link: AppLink.claimInviteReward,
|
||||
payload: {
|
||||
'invite_id': (driverInvitationData[index]['id']).toString(),
|
||||
'driver_id': box.read(BoxName.driverID).toString(),
|
||||
'country_code': box.read(BoxName.countryCode).toString(),
|
||||
}
|
||||
);
|
||||
|
||||
if (response != 'failure') {
|
||||
var data = jsonDecode(response);
|
||||
if (data['status'] == 'success') {
|
||||
NotificationController().showNotification(
|
||||
"You have got a gift for invitation".tr,
|
||||
'${"You have 500".tr} ${CurrencyHelper.currency}',
|
||||
'tone1',
|
||||
'');
|
||||
// refresh stats
|
||||
fetchDriverStats();
|
||||
} else {
|
||||
mySnackbarError(data['message'] ?? 'Claim failed'.tr);
|
||||
}
|
||||
}
|
||||
} 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', '200', '200');
|
||||
await Get.find<CaptainWalletController>()
|
||||
.addDriverWalletToInvitor('paymentMethod',
|
||||
driverInvitationData[index]['driverInviterId'], '200');
|
||||
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 200".tr} ${CurrencyHelper.currency}',
|
||||
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());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// removed normalizeSyrianPhone
|
||||
|
||||
/// Sends an invitation to a potential new driver.
|
||||
void sendInvite() async {
|
||||
if (invitePhoneController.text.isEmpty) {
|
||||
mySnackbarError('Please enter a phone number'.tr);
|
||||
return;
|
||||
}
|
||||
String formattedPhoneNumber =
|
||||
CountryLogic.formatCurrentCountryPhone(invitePhoneController.text);
|
||||
if (formattedPhoneNumber.length != 12) {
|
||||
mySnackbarError('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' && response is Map) {
|
||||
var d = response;
|
||||
mySnackbarSuccess('Invite sent successfully'.tr);
|
||||
String inviteCode = d['message']['inviteCode'].toString();
|
||||
String message = '${'*Siro 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 24 hours".tr}\n\n'
|
||||
'${'before'.tr} *${d['message']['expirationTime'].toString()}*\n\n'
|
||||
'${"Code:".tr} *${inviteCode}*\n\n'
|
||||
'${"Quick Invite Link:".tr}\n'
|
||||
'${AppLink.inviteRedirectUrl}?code=$inviteCode&app=driver\n\n'
|
||||
'${"💡 Note: If the link above is not clickable, please save this contact or reply to this message to activate all links.".tr}';
|
||||
|
||||
launchCommunication('whatsapp', formattedPhoneNumber, message);
|
||||
invitePhoneController.clear();
|
||||
} else {
|
||||
mySnackbarError("Invite code already used".tr);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends an invitation to a potential new passenger.
|
||||
void sendInviteToPassenger() async {
|
||||
if (invitePhoneController.text.isEmpty) {
|
||||
mySnackbarError('Please enter a phone number'.tr);
|
||||
return;
|
||||
}
|
||||
|
||||
String formattedPhoneNumber = CountryLogic.formatCurrentCountryPhone(invitePhoneController.text);
|
||||
|
||||
if (formattedPhoneNumber.length < 12) {
|
||||
// +963 + 9 digits = 12+
|
||||
mySnackbarError('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' && response is Map) {
|
||||
var d = response;
|
||||
mySnackbarSuccess('Invite sent successfully'.tr);
|
||||
String inviteCode = d['message']['inviteCode'].toString();
|
||||
|
||||
String message = '${'*Siro 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 24 hours".tr}\n\n'
|
||||
'${'before'.tr} *${d['message']['expirationTime'].toString()}*\n\n'
|
||||
'${"Code:".tr} *${inviteCode}*\n\n'
|
||||
'${"Quick Invite Link:".tr}\n'
|
||||
'${AppLink.inviteRedirectUrl}?code=$inviteCode&app=rider\n\n'
|
||||
'${"💡 Note: If the link above is not clickable, please save this contact or reply to this message to activate all links.".tr}';
|
||||
|
||||
launchCommunication('whatsapp', formattedPhoneNumber, message);
|
||||
invitePhoneController.clear();
|
||||
} else {
|
||||
mySnackbarError("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,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,718 @@
|
||||
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:siro_driver/views/auth/syria/registration_view.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/home/Captin/home_captain/home_captin.dart';
|
||||
import 'package:location/location.dart';
|
||||
|
||||
import '../../../constant/api_key.dart';
|
||||
import '../../../constant/info.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/firbase_messge.dart';
|
||||
import '../../firebase/local_notification.dart';
|
||||
import '../../firebase/notification_service.dart';
|
||||
import '../../functions/country_logic.dart';
|
||||
import '../../functions/encrypt_decrypt.dart';
|
||||
import '../../functions/package_info.dart';
|
||||
import '../../functions/secure_storage.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 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 {
|
||||
// كشف الدولة تلقائياً عبر الموقع الجغرافي
|
||||
await CountryLogic.initializeCountry();
|
||||
// box.write(BoxName.driverID, '34feffd3fa72d6bee56b');
|
||||
// await getAppTester();
|
||||
await 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 {
|
||||
// If the phone is already verified locally and we have a driver ID, skip the API check
|
||||
// This prevents asking for OTP again if the user restarts the app during registration.
|
||||
if (box.read(BoxName.phoneVerified) == '1' &&
|
||||
box.read(BoxName.driverID) != null) {
|
||||
Get.offAll(() => RegistrationView());
|
||||
return;
|
||||
}
|
||||
|
||||
var res = await CRUD().post(
|
||||
link: AppLink.isPhoneVerified,
|
||||
payload: {'phone_number': box.read(BoxName.phoneDriver)});
|
||||
|
||||
if (res != 'failure') {
|
||||
// Get.offAll(() => SyrianCardAI());
|
||||
Get.offAll(() => RegistrationView());
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
String shortHash(String password) {
|
||||
var bytes = utf8.encode(password);
|
||||
var digest = sha256.convert(bytes);
|
||||
return base64UrlEncode(digest.bytes);
|
||||
}
|
||||
|
||||
var dev = '';
|
||||
Future<String>? _walletJwtFuture;
|
||||
|
||||
getJwtWallet() async {
|
||||
if (_walletJwtFuture != null) {
|
||||
Log.print('⏳ getJwtWallet: Request already in progress, reusing future.');
|
||||
return _walletJwtFuture!;
|
||||
}
|
||||
|
||||
_walletJwtFuture = _getJwtWalletInternal();
|
||||
try {
|
||||
return await _walletJwtFuture!;
|
||||
} finally {
|
||||
_walletJwtFuture = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _getJwtWalletInternal() async {
|
||||
if (box.read(BoxName.security_check).toString() != 'passed') {
|
||||
Log.print('Security check failed');
|
||||
return '';
|
||||
}
|
||||
|
||||
// 1. Check GetStorage first to avoid redundant API calls
|
||||
String? cachedJwt = box.read(BoxName.walletJwt)?.toString();
|
||||
if (cachedJwt != null && cachedJwt.isNotEmpty) {
|
||||
bool isTokenValid = false;
|
||||
try {
|
||||
final parts = cachedJwt.split('.');
|
||||
if (parts.length == 3) {
|
||||
var payload = parts[1];
|
||||
switch (payload.length % 4) {
|
||||
case 2:
|
||||
payload += '==';
|
||||
break;
|
||||
case 3:
|
||||
payload += '=';
|
||||
break;
|
||||
}
|
||||
final decoded = jsonDecode(utf8.decode(base64Url.decode(payload)));
|
||||
final exp = decoded['exp'];
|
||||
if (exp != null) {
|
||||
// Check if token has at least 10 seconds remaining
|
||||
isTokenValid =
|
||||
DateTime.now().millisecondsSinceEpoch < (exp * 1000 - 10000);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
if (isTokenValid) {
|
||||
Log.print('🔑 Valid Wallet JWT found in storage. Skipping generation.');
|
||||
return cachedJwt;
|
||||
}
|
||||
}
|
||||
|
||||
Log.print('Security check passed');
|
||||
dev = Platform.isAndroid ? 'android' : 'ios';
|
||||
var fingerPrint = box.read(BoxName.deviceFingerprint)?.toString() ??
|
||||
await DeviceHelper.getDeviceFingerprint();
|
||||
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.body: ${response1.body}');
|
||||
var decoded = jsonDecode(response1.body);
|
||||
var jwt = decoded['message'] is Map && decoded['message']['jwt'] != null
|
||||
? decoded['message']['jwt']
|
||||
: decoded['jwt'];
|
||||
var hmac = decoded['message'] is Map && decoded['message']['hmac'] != null
|
||||
? decoded['message']['hmac']
|
||||
: decoded['hmac'];
|
||||
Log.print('payment["jwt"]: $jwt');
|
||||
|
||||
if (jwt != null) {
|
||||
await box.write(BoxName.walletJwt, jwt.toString());
|
||||
await box.write(BoxName.hmac, hmac);
|
||||
return jwt.toString();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
getJWT() async {
|
||||
await EncryptionHelper.initialize();
|
||||
|
||||
// 1. Check secure storage first to avoid redundant API calls
|
||||
String? secureJwt = await storage.read(key: BoxName.jwt);
|
||||
if (secureJwt != null && secureJwt.isNotEmpty) {
|
||||
bool isTokenValid = false;
|
||||
try {
|
||||
final parts = secureJwt.split('.');
|
||||
if (parts.length == 3) {
|
||||
String payload = parts[1];
|
||||
switch (payload.length % 4) {
|
||||
case 2:
|
||||
payload += '==';
|
||||
break;
|
||||
case 3:
|
||||
payload += '=';
|
||||
break;
|
||||
}
|
||||
final decoded = jsonDecode(utf8.decode(base64Url.decode(payload)));
|
||||
final exp = decoded['exp'];
|
||||
if (exp != null) {
|
||||
// Check if token is valid with a 30-second buffer
|
||||
isTokenValid =
|
||||
DateTime.now().millisecondsSinceEpoch < (exp * 1000 - 30000);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
if (isTokenValid) {
|
||||
Log.print('🔑 Valid JWT found in secure storage. Skipping generation.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
'fingerPrint': box.read(BoxName.deviceFingerprint) ??
|
||||
await DeviceHelper.getDeviceFingerprint(),
|
||||
};
|
||||
// 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}');
|
||||
|
||||
String? jwt;
|
||||
if (decodedResponse1['message'] is Map &&
|
||||
decodedResponse1['message']['jwt'] != null) {
|
||||
jwt = decodedResponse1['message']['jwt'];
|
||||
} else {
|
||||
jwt = decodedResponse1['jwt'];
|
||||
}
|
||||
|
||||
if (jwt != null) {
|
||||
// box.write(BoxName.jwt, c(jwt));
|
||||
await storage.write(key: BoxName.jwt, value: 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',
|
||||
'fingerPrint': box.read(BoxName.deviceFingerprint) ??
|
||||
await DeviceHelper.getDeviceFingerprint(),
|
||||
};
|
||||
// print(payload);
|
||||
var response1 = await http.post(
|
||||
Uri.parse(AppLink.loginJwtDriver),
|
||||
body: payload,
|
||||
);
|
||||
Log.print('response1.request: ${response1.request}');
|
||||
Log.print('response1.body: ${response1.body}');
|
||||
|
||||
if (response1.statusCode == 200) {
|
||||
final decodedResponse1 = jsonDecode(response1.body);
|
||||
// Log.print('decodedResponse1: ${decodedResponse1}');
|
||||
|
||||
String? jwt;
|
||||
if (decodedResponse1['message'] is Map &&
|
||||
decodedResponse1['message']['jwt'] != null) {
|
||||
jwt = decodedResponse1['message']['jwt'];
|
||||
} else {
|
||||
jwt = decodedResponse1['jwt'];
|
||||
}
|
||||
|
||||
if (jwt != null) {
|
||||
// await box.write(BoxName.jwt, c(jwt));
|
||||
await storage.write(key: BoxName.jwt, value: jwt);
|
||||
}
|
||||
|
||||
// await AppInitializer().getKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getLocationPermission() async {
|
||||
var status = await Permission.locationAlways.status;
|
||||
if (!status.isGranted) {
|
||||
await Permission.locationAlways.request();
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
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}');
|
||||
|
||||
// حماية من النوع — res قد يكون String ('failure'/'token_expired') بدل Map
|
||||
if (res is! Map) return;
|
||||
|
||||
if (res['status'] != 'failure') {
|
||||
isInviteDriverFound = true;
|
||||
update();
|
||||
box.write(BoxName.isInstall, '1');
|
||||
NotificationController().showNotification(
|
||||
"Code approved".tr, "Code approved".tr, 'tone2', '');
|
||||
|
||||
try {
|
||||
NotificationService.sendNotification(
|
||||
target: (res)['message'][0]['token'].toString(),
|
||||
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!',
|
||||
);
|
||||
} catch (e) {
|
||||
Log.print('invite notification error: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loginDriver(String driverID, email) async {
|
||||
isloading = true;
|
||||
update();
|
||||
// await SecurityHelper.performSecurityChecks();
|
||||
// Log.print('(BoxName.emailDriver): ${box.read(BoxName.emailDriver)}');
|
||||
// await getJWT();
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.loginFromGoogleCaptin, payload: {'driver_id': driverID});
|
||||
Log.print('loginDriver: ${res}');
|
||||
if (res == 'failure') {
|
||||
await isPhoneVerified();
|
||||
isloading = false; // <--- أضفت هذا أيضاً
|
||||
update();
|
||||
return false;
|
||||
// Get.snackbar('Failure', '', backgroundColor: Colors.red);
|
||||
} else {
|
||||
var jsonDecoeded = jsonDecode(res);
|
||||
var d = jsonDecoeded['data'][0];
|
||||
if (jsonDecoeded.isNotEmpty) {
|
||||
if (jsonDecoeded['status'] == 'success' &&
|
||||
d['is_verified'].toString() == '1') {
|
||||
box.write(BoxName.emailDriver, d['email']);
|
||||
box.write(BoxName.firstTimeLoadKey, 'false');
|
||||
box.write(BoxName.driverID, (d['id']));
|
||||
box.write(BoxName.isTest, '1');
|
||||
box.write(BoxName.gender, (d['gender']));
|
||||
box.write(BoxName.phoneVerified, d['is_verified'].toString());
|
||||
box.write(BoxName.phoneDriver, (d['phone']));
|
||||
box.write(BoxName.is_claimed, d['is_claimed']);
|
||||
box.write(BoxName.isInstall, d['isInstall']);
|
||||
// box.write(
|
||||
// BoxName.isGiftToken, d['isGiftToken']);
|
||||
box.write(BoxName.nameArabic, (d['name_arabic']));
|
||||
box.write(BoxName.carYear, d['year']);
|
||||
box.write(BoxName.bankCodeDriver, (d['bankCode']));
|
||||
box.write(BoxName.accountBankNumberDriver, (d['accountBank']));
|
||||
box.write(
|
||||
BoxName.nameDriver,
|
||||
'${(d['first_name'])}'
|
||||
' ${(d['last_name'])}');
|
||||
if (((d['model']).toString().contains('دراجه') ||
|
||||
d['make'].toString().contains('دراجه '))) {
|
||||
if ((d['gender']).toString() == 'Male') {
|
||||
box.write(BoxName.carTypeOfDriver, 'Scooter');
|
||||
} else {
|
||||
box.write(BoxName.carTypeOfDriver, 'Pink Bike');
|
||||
}
|
||||
} else if (int.parse(d['year'].toString()) > 2016) {
|
||||
if (d['gender'].toString() != 'Male') {
|
||||
box.write(BoxName.carTypeOfDriver, 'Lady');
|
||||
} else {
|
||||
box.write(BoxName.carTypeOfDriver, 'Comfort');
|
||||
}
|
||||
} else if (int.parse(d['year'].toString()) > 2002 &&
|
||||
int.parse(d['year'].toString()) < 2016) {
|
||||
box.write(BoxName.carTypeOfDriver, 'Speed');
|
||||
} else if (int.parse(d['year'].toString()) < 2002) {
|
||||
box.write(BoxName.carTypeOfDriver, 'Awfar Car');
|
||||
}
|
||||
|
||||
// ✅ الحصول على توكن access بدل registration قبل أي طلبات بعد تسجيل الدخول
|
||||
Log.print('🔑 Getting access token after login...');
|
||||
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
||||
// await storage.write(
|
||||
// key: BoxName.fingerPrint, value: fingerPrint.toString());
|
||||
// await getJWT();
|
||||
// Log.print('🔑 Access token obtained.');
|
||||
|
||||
// add invitations
|
||||
if (box.read(BoxName.isInstall) == null ||
|
||||
box.read(BoxName.isInstall).toString() == '0') {
|
||||
updateInvitationCodeFromRegister();
|
||||
}
|
||||
|
||||
// updateAppTester(AppInformation.appName);
|
||||
final driverStatus = d['status'].toString();
|
||||
if (driverStatus != 'yet' && driverStatus != 'pending_review') {
|
||||
var token = await CRUD().get(
|
||||
link: AppLink.getDriverToken,
|
||||
payload: {
|
||||
'captain_id': (box.read(BoxName.driverID)).toString()
|
||||
});
|
||||
// print(jsonDecode(token)['data'][0]['token'].toString());
|
||||
// print(box.read(BoxName.tokenDriver).toString());
|
||||
// if (box.read(BoxName.emailDriver).toString() !=
|
||||
// '963992952235@intaleqapp.com') {
|
||||
if (token != 'failure') {
|
||||
var serverData = jsonDecode(token);
|
||||
final serverToken = serverData['data'][0]['token'].toString();
|
||||
final storedToken =
|
||||
box.read(BoxName.tokenDriver)?.toString() ?? '';
|
||||
final serverFp = serverData['data'][0]['fingerPrint'].toString();
|
||||
final tokenMismatch =
|
||||
storedToken.isNotEmpty && serverToken != storedToken;
|
||||
Log.print(
|
||||
'🔍 [FP_COMPARE] serverFp: $serverFp | localFp: ${fingerPrint.toString()} | tokenMismatch: $tokenMismatch | serverToken: $serverToken | storedToken: $storedToken');
|
||||
if (tokenMismatch) {
|
||||
Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: 'Device Change Detected'.tr,
|
||||
middleText: 'Please verify your identity'.tr,
|
||||
textConfirm: 'Verify'.tr,
|
||||
confirmTextColor: Colors.white,
|
||||
onConfirm: () {
|
||||
// نغلق الـ Dialog أولاً بشكل صريح
|
||||
if (Get.isDialogOpen ?? false) {
|
||||
Get.back();
|
||||
}
|
||||
|
||||
// ثم ننتقل لصفحة OTP
|
||||
Get.offAll(
|
||||
() => OtpVerificationPage(
|
||||
phone: d['phone'].toString(),
|
||||
deviceToken: fingerPrint.toString(),
|
||||
token: token.toString(),
|
||||
ptoken:
|
||||
jsonDecode(token)['data'][0]['token'].toString(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
isloading = false;
|
||||
update();
|
||||
return true; // نخرج من الدالة هنا لنسمح لـ OTP بالتعامل مع الأمر
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
Get.offAll(() => HomeCaptain()); // افترض أن هذا الكلاس موجود
|
||||
isloading = false; // <--- أضفت هذا
|
||||
update(); // <--- أضفت هذا
|
||||
return true;
|
||||
} else {
|
||||
Get.offAll(
|
||||
() => DriverVerificationScreen()); // افترض أن هذا الكلاس موجود
|
||||
isloading = false; // <--- أضفت هذا
|
||||
update(); // <--- أضفت هذا
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get.off(() => HomeCaptain());
|
||||
} else {
|
||||
Get.offAll(() => PhoneNumberScreen());
|
||||
isloading = false;
|
||||
update();
|
||||
return false; // <--- ✅ وهذا السطر موجود للحالات الأخرى
|
||||
}
|
||||
} else {
|
||||
mySnackbarSuccess('');
|
||||
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.loginFromGoogleCaptin, payload: {'driver_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()) > 2016) {
|
||||
if (d['gender'].toString() != 'Male') {
|
||||
box.write(BoxName.carTypeOfDriver, 'Lady');
|
||||
} else {
|
||||
box.write(BoxName.carTypeOfDriver, 'Comfort');
|
||||
}
|
||||
} else if (int.parse(d['year'].toString()) > 2002 &&
|
||||
int.parse(d['year'].toString()) < 2016) {
|
||||
box.write(BoxName.carTypeOfDriver, 'Speed');
|
||||
} else if (int.parse(d['year'].toString()) < 2002) {
|
||||
box.write(BoxName.carTypeOfDriver, 'Awfar Car');
|
||||
}
|
||||
// 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, email) async {
|
||||
isloading = true;
|
||||
update();
|
||||
|
||||
try {
|
||||
var fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
||||
var payload = {
|
||||
'email': email,
|
||||
'password': password,
|
||||
'fingerPrint': fingerPrint,
|
||||
'aud': AppInformation.appName,
|
||||
};
|
||||
|
||||
var response = await http.post(
|
||||
Uri.parse(AppLink.loginUsingCredentialsWithoutGoogle),
|
||||
body: payload,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
var jsonDecoeded = jsonDecode(response.body);
|
||||
if (jsonDecoeded['status'] == 'success') {
|
||||
var d = jsonDecoeded['data'][0];
|
||||
var jwt = jsonDecoeded['jwt'];
|
||||
|
||||
// حفظ التوكن أولاً
|
||||
if (jwt != null) {
|
||||
box.write(BoxName.jwt, c(jwt));
|
||||
await storage.write(key: BoxName.jwt, value: c(jwt));
|
||||
}
|
||||
|
||||
box.write(BoxName.emailDriver, (d['email']));
|
||||
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.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'])}');
|
||||
|
||||
await storage.write(key: BoxName.fingerPrint, value: fingerPrint);
|
||||
|
||||
Get.off(() => HomeCaptain());
|
||||
} else {
|
||||
mySnackbarError('Login failed'.tr);
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
} else {
|
||||
mySnackbarError('Server error'.tr);
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
} catch (e) {
|
||||
mySnackbarError('Network error'.tr);
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
});
|
||||
CRUD().post(
|
||||
link: "${AppLink.seferAlexandriaServer}/ride/firebase/addDriver.php",
|
||||
payload: {
|
||||
'token': box.read(BoxName.tokenDriver),
|
||||
'captain_id': box.read(BoxName.driverID).toString()
|
||||
});
|
||||
CRUD().post(
|
||||
link: "${AppLink.seferGizaServer}/ride/firebase/addDriver.php",
|
||||
payload: {
|
||||
'token': box.read(BoxName.tokenDriver),
|
||||
'captain_id': box.read(BoxName.driverID).toString()
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
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:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_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();
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,127 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/print.dart';
|
||||
import 'package:siro_driver/views/home/Captin/home_captain/home_captin.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../views/widgets/error_snakbar.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/otp/request.php',
|
||||
payload: {
|
||||
'receiver': phone,
|
||||
'context': 'token_change',
|
||||
'user_type': 'driver',
|
||||
// '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 = box.read(BoxName.deviceFingerprint);
|
||||
try {
|
||||
final response = await CRUD().post(
|
||||
link: '${AppLink.server}/auth/otp/verify.php',
|
||||
payload: {
|
||||
'phone_number': phone,
|
||||
'token_code': otpCode.value,
|
||||
'context': 'token_change',
|
||||
'user_type': 'driver',
|
||||
'token': box.read(BoxName.tokenDriver).toString(),
|
||||
'fingerPrint': finger.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
if (response != 'failure' &&
|
||||
response != 'token_expired' &&
|
||||
response != 'no_internet') {
|
||||
Log.print('response (already decoded): ${response}');
|
||||
|
||||
// توجه إلى الصفحة التالية
|
||||
await CRUD().post(
|
||||
link: AppLink.addTokensDriver,
|
||||
payload: {
|
||||
'token': box.read(BoxName.tokenDriver).toString(),
|
||||
'fingerPrint': finger.toString(),
|
||||
'captain_id': box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
|
||||
await NotificationService.sendNotification(
|
||||
target: ptoken.toString(),
|
||||
title: 'token change'.tr,
|
||||
body: 'token change'.tr,
|
||||
isTopic: false,
|
||||
tone: 'cancel',
|
||||
driverList: [],
|
||||
category: 'token change',
|
||||
);
|
||||
|
||||
Get.offAll(() => HomeCaptain());
|
||||
} else {
|
||||
mySnackbarError('OTP is incorrect or expired'.tr);
|
||||
}
|
||||
} catch (e) {
|
||||
mySnackbarError(e.toString());
|
||||
} finally {
|
||||
isVerifying.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/controller/auth/captin/login_captin_controller.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/print.dart';
|
||||
import 'package:siro_driver/views/home/on_boarding_page.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:siro_driver/controller/functions/country_logic.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../views/auth/syria/registration_view.dart';
|
||||
|
||||
// --- Helper Class for Phone Authentication ---
|
||||
|
||||
class PhoneAuthHelper {
|
||||
// Define your server URLs
|
||||
static final String _sendOtpUrl = '${AppLink.server}/auth/otp/request.php';
|
||||
static final String _verifyOtpUrl = '${AppLink.server}/auth/otp/verify.php';
|
||||
static final String _registerUrl =
|
||||
'${AppLink.server}/auth/syria/driver/register_driver.php';
|
||||
// removed formatSyrianPhone
|
||||
|
||||
/// Sends an OTP to the provided phone number.
|
||||
static Future<bool> sendOtp(String phoneNumber) async {
|
||||
try {
|
||||
final detectedCountry = AppLink.detectCountryFromPhone(phoneNumber);
|
||||
if (detectedCountry.isNotEmpty) {
|
||||
await box.write(BoxName.countryCode, detectedCountry);
|
||||
}
|
||||
final fixedPhone = CountryLogic.formatCurrentCountryPhone(phoneNumber);
|
||||
Log.print('fixedPhone: $fixedPhone');
|
||||
|
||||
final response = await CRUD().post(
|
||||
link: _sendOtpUrl,
|
||||
payload: {
|
||||
'receiver': fixedPhone,
|
||||
'context': 'login',
|
||||
'user_type': 'driver'
|
||||
},
|
||||
);
|
||||
Log.print('fixedPhone: ${fixedPhone}');
|
||||
|
||||
if (response != 'failure') {
|
||||
final data = (response);
|
||||
Log.print('data: ${data}');
|
||||
if (data['status'] == 'success') {
|
||||
mySnackbarSuccess('An OTP has been sent to your number.'.tr);
|
||||
return true;
|
||||
} else {
|
||||
mySnackbarError(data['message'] ?? 'Failed to send OTP.'.tr);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
mySnackbarError('Server error. Please try again.'.tr);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
// mySnackbarError('An error occurred: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies the OTP and logs the user in.
|
||||
static Future<void> verifyOtp(String phoneNumber, String otpCode) async {
|
||||
try {
|
||||
final fixedPhone = CountryLogic.formatCurrentCountryPhone(phoneNumber);
|
||||
Log.print('fixedPhone: $fixedPhone');
|
||||
final response = await CRUD().post(
|
||||
link: _verifyOtpUrl,
|
||||
payload: {
|
||||
'phone_number': fixedPhone,
|
||||
'token_code': otpCode,
|
||||
'context': 'login',
|
||||
'user_type': 'driver'
|
||||
},
|
||||
);
|
||||
|
||||
if (response != 'failure') {
|
||||
final data = response;
|
||||
|
||||
if (data['status'] == 'success') {
|
||||
final isRegistered = data['message']['isRegistered'] ?? false;
|
||||
box.write(BoxName.phoneVerified, '1');
|
||||
box.write(BoxName.phoneDriver, phoneNumber);
|
||||
box.write(BoxName.driverID, data['message']['driverID']);
|
||||
|
||||
if (isRegistered) {
|
||||
// ✅ السائق مسجل مسبقًا - سجل دخوله واذهب إلى الصفحة الرئيسية
|
||||
final driver = data['message']['driver'];
|
||||
// mySnackbarSuccess('Welcome back, ${driver['first_name']}!');
|
||||
|
||||
// حفظ بيانات السائق إذا أردت:
|
||||
box.write(BoxName.driverID, driver['id']);
|
||||
box.write(BoxName.emailDriver, driver['email']);
|
||||
|
||||
await Get.find<LoginDriverController>().loginDriver(
|
||||
driver['id'].toString(), driver['email'].toString());
|
||||
} else {
|
||||
// ✅ رقم الهاتف تم التحقق منه لكن السائق غير مسجل
|
||||
// mySnackbarSuccess('Phone verified. Please complete registration.');
|
||||
// Get.offAll(() => SyrianCardAI());
|
||||
Get.to(() => RegistrationView());
|
||||
}
|
||||
} else {
|
||||
mySnackbarError(data['message'] ?? 'Verification failed.');
|
||||
}
|
||||
} else {
|
||||
mySnackbarError('Server error. Please try again.');
|
||||
}
|
||||
} catch (e) {
|
||||
mySnackbarError('An error occurred: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> registerUser({
|
||||
required String phoneNumber,
|
||||
required String firstName,
|
||||
required String lastName,
|
||||
String? email,
|
||||
}) async {
|
||||
try {
|
||||
final response = await CRUD().post(
|
||||
link: _registerUrl,
|
||||
payload: {
|
||||
'phone_number': phoneNumber,
|
||||
'first_name': firstName,
|
||||
'last_name': lastName,
|
||||
'email': email ?? '', // Send empty string if null
|
||||
},
|
||||
);
|
||||
final data = (response);
|
||||
if (data != 'failure') {
|
||||
// Registration successful, log user in
|
||||
await _handleSuccessfulLogin(data['message']);
|
||||
} else {
|
||||
mySnackbarError(
|
||||
"User with this phone number or email already exists.".tr);
|
||||
}
|
||||
} catch (e) {
|
||||
mySnackbarError('An error occurred: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _handleSuccessfulLogin(
|
||||
Map<String, dynamic> userData) async {
|
||||
mySnackbarSuccess('Welcome, ${userData['first_name']}!');
|
||||
|
||||
// Save user data to local storage (Hive box) using new keys
|
||||
box.write(BoxName.passengerID, userData['id']);
|
||||
box.write(BoxName.nameDriver, userData['first_name']);
|
||||
box.write(BoxName.lastNameDriver, userData['last_name']);
|
||||
box.write(BoxName.emailDriver, userData['email']);
|
||||
box.write(BoxName.phoneDriver, userData['phone']);
|
||||
|
||||
Get.offAll(() => OnBoardingPage()); // Navigate to home
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
|
||||
import '../../models/model/onboarding_model.dart';
|
||||
import '../../views/auth/captin/login_captin.dart';
|
||||
|
||||
abstract class OnBoardingController extends GetxController {
|
||||
next();
|
||||
onPageChanged(int index);
|
||||
}
|
||||
|
||||
class OnBoardingControllerImp extends OnBoardingController {
|
||||
late PageController pageController;
|
||||
|
||||
int currentPage = 0;
|
||||
|
||||
@override
|
||||
next() {
|
||||
currentPage++;
|
||||
|
||||
if (currentPage > onBoardingList.length - 1) {
|
||||
box.write(BoxName.onBoarding, 'yes');
|
||||
Get.offAll(() => LoginCaptin());
|
||||
} else {
|
||||
pageController.animateToPage(currentPage,
|
||||
duration: const Duration(milliseconds: 900), curve: Curves.easeInOut);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
onPageChanged(int index) {
|
||||
currentPage = index;
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
pageController = PageController();
|
||||
super.onInit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,883 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:image_cropper/image_cropper.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:path/path.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/firebase/notification_service.dart';
|
||||
import '../../../constant/box_name.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
// --- Final Submission ---
|
||||
import 'package:path_provider/path_provider.dart' as path_provider;
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/info.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../print.dart';
|
||||
import '../../../views/widgets/error_snakbar.dart';
|
||||
import '../../functions/crud.dart';
|
||||
import '../../functions/encrypt_decrypt.dart';
|
||||
import '../../functions/package_info.dart';
|
||||
import '../captin/login_captin_controller.dart';
|
||||
|
||||
// You can create a simple enum to manage image types
|
||||
enum ImageType {
|
||||
driverLicenseFront,
|
||||
driverLicenseBack,
|
||||
carLicenseFront,
|
||||
carLicenseBack,
|
||||
}
|
||||
|
||||
class RegistrationController extends GetxController {
|
||||
// Page Controller for managing steps
|
||||
late PageController pageController;
|
||||
var currentPage =
|
||||
0.obs; // Use .obs for reactive updates on the step indicator
|
||||
|
||||
// Loading state
|
||||
var isLoading = false.obs;
|
||||
var isloading = false;
|
||||
CroppedFile? croppedFile;
|
||||
final picker = ImagePicker();
|
||||
var image;
|
||||
File? myImage;
|
||||
String? colorHex; // سيُملى من الدروب داون
|
||||
// Form Keys for validation
|
||||
final driverInfoFormKey = GlobalKey<FormState>();
|
||||
final carInfoFormKey = GlobalKey<FormState>();
|
||||
|
||||
// STEP 1: Driver Information Controllers
|
||||
final firstNameController = TextEditingController();
|
||||
final lastNameController = TextEditingController();
|
||||
final nationalIdController = TextEditingController();
|
||||
final bithdateController = TextEditingController();
|
||||
final phoneController = TextEditingController(); // You can pre-fill this
|
||||
final driverLicenseExpiryController = TextEditingController();
|
||||
DateTime? driverLicenseExpiryDate;
|
||||
final siteController = TextEditingController(); // المدينة/الموقع
|
||||
|
||||
// STEP 2: Car Information Controllers
|
||||
final carPlateController = TextEditingController();
|
||||
final carMakeController = TextEditingController();
|
||||
final carModelController = TextEditingController();
|
||||
final carYearController = TextEditingController();
|
||||
final carColorController = TextEditingController();
|
||||
final carVinController = TextEditingController(); // Chassis number
|
||||
final carRegistrationExpiryController = TextEditingController();
|
||||
DateTime? carRegistrationExpiryDate;
|
||||
// داخل RegistrationController
|
||||
|
||||
// المتغيرات لتخزين القيم المختارة (لإرسالها للـ API لاحقاً)
|
||||
int? selectedVehicleCategoryId; // سيخزن 1 أو 2 أو 3
|
||||
int? selectedFuelTypeId; // سيخزن 1 أو 2 أو 3 أو 4
|
||||
|
||||
// قائمة أنواع المركبات (مطابقة لقاعدة البيانات)
|
||||
final List<Map<String, dynamic>> vehicleCategoryOptions = [
|
||||
{'id': 1, 'name': 'Car'.tr}, // ترجمة: سيارة
|
||||
{'id': 2, 'name': 'Motorcycle'.tr}, // ترجمة: دراجة نارية
|
||||
{'id': 3, 'name': 'Van / Bus'.tr}, // ترجمة: فان / باص
|
||||
];
|
||||
|
||||
// قائمة أنواع الوقود
|
||||
final List<Map<String, dynamic>> fuelTypeOptions = [
|
||||
{'id': 1, 'name': 'Petrol'.tr}, // ترجمة: بنزين
|
||||
{'id': 2, 'name': 'Diesel'.tr}, // ترجمة: ديزل
|
||||
{'id': 3, 'name': 'Electric'.tr}, // ترجمة: كهربائي
|
||||
{'id': 4, 'name': 'Hybrid'.tr}, // ترجمة: هايبرد
|
||||
];
|
||||
// STEP 3: Document Uploads
|
||||
File? driverLicenseFrontImage;
|
||||
File? driverLicenseBackImage;
|
||||
File? carLicenseFrontImage;
|
||||
File? carLicenseBackImage;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
pageController = PageController();
|
||||
// Pre-fill phone number if it exists in storage
|
||||
// phoneController.text = box.read(BoxName.phoneDriver) ?? '';
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
pageController.dispose();
|
||||
// Dispose all other text controllers
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// --- Page Navigation ---
|
||||
void goToNextStep() {
|
||||
bool isValid = false;
|
||||
if (currentPage.value == 0) {
|
||||
// Validate Step 1
|
||||
isValid = driverInfoFormKey.currentState!.validate();
|
||||
if (isValid) {
|
||||
// Optional: Check if license is expired
|
||||
// if (driverLicenseExpiryDate != null &&
|
||||
// driverLicenseExpiryDate!.isBefore(DateTime.now())) {
|
||||
// Get.snackbar('Expired License', 'Your driver’s license has expired.'.tr
|
||||
// ,
|
||||
// snackPosition: SnackPosition.BOTTOM,
|
||||
// backgroundColor: Colors.red,
|
||||
// colorText: Colors.white);
|
||||
// return; // Stop progression
|
||||
// }
|
||||
}
|
||||
} else if (currentPage.value == 1) {
|
||||
// Validate Step 2
|
||||
isValid = carInfoFormKey.currentState!.validate();
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeIn,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void goToPreviousStep() {
|
||||
pageController.previousPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeIn,
|
||||
);
|
||||
}
|
||||
|
||||
// --- Image Picking ---
|
||||
Future<void> pickImage(ImageType type) async {
|
||||
try {
|
||||
final picker = ImagePicker();
|
||||
final picked = await picker.pickImage(
|
||||
source: ImageSource.camera,
|
||||
imageQuality: 95, // جودة أولية من الكاميرا
|
||||
maxWidth: 3000, // نسمح بصورة كبيرة ثم نصغّر نحن
|
||||
);
|
||||
if (picked == null) return;
|
||||
|
||||
// قصّ الصورة
|
||||
final cropped = await ImageCropper().cropImage(
|
||||
sourcePath: picked.path,
|
||||
uiSettings: [
|
||||
AndroidUiSettings(
|
||||
toolbarTitle: 'Cropper'.tr,
|
||||
toolbarColor: AppColor.accentColor,
|
||||
toolbarWidgetColor: AppColor.redColor,
|
||||
initAspectRatio: CropAspectRatioPreset.original,
|
||||
lockAspectRatio: false,
|
||||
),
|
||||
IOSUiSettings(title: 'Cropper'.tr),
|
||||
],
|
||||
);
|
||||
if (cropped == null) return; // المستخدم ألغى
|
||||
|
||||
// قراءة bytes + التصحيح حسب EXIF ثم التصغير
|
||||
final rawBytes = await File(cropped.path).readAsBytes();
|
||||
final decoded = img.decodeImage(rawBytes);
|
||||
if (decoded == null) throw Exception('Decode image failed');
|
||||
|
||||
// تصحيح اتجاه الصورة (EXIF)
|
||||
final fixed = img.bakeOrientation(decoded);
|
||||
|
||||
// تصغير لعرض 800px (عدّل عند الحاجة)
|
||||
final resized = img.copyResize(fixed, width: 800);
|
||||
|
||||
// حفظ مؤقت بصيغة JPG
|
||||
final tmpDir = await getTemporaryDirectory();
|
||||
final outPath =
|
||||
'${tmpDir.path}/doc_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||
final outFile = File(outPath);
|
||||
await outFile.writeAsBytes(img.encodeJpg(resized, quality: 85));
|
||||
|
||||
// عيّن الملف في المتغير الصحيح حسب النوع
|
||||
if (outFile != null) {
|
||||
switch (type) {
|
||||
case ImageType.driverLicenseFront:
|
||||
driverLicenseFrontImage = File(outFile.path);
|
||||
break;
|
||||
case ImageType.driverLicenseBack:
|
||||
driverLicenseBackImage = File(outFile.path);
|
||||
break;
|
||||
case ImageType.carLicenseFront:
|
||||
carLicenseFrontImage = File(outFile.path);
|
||||
break;
|
||||
case ImageType.carLicenseBack:
|
||||
carLicenseBackImage = File(outFile.path);
|
||||
break;
|
||||
}
|
||||
update(); // Use update() to refresh the GetBuilder UI
|
||||
}
|
||||
|
||||
update(); // لتحديث الـ UI
|
||||
|
||||
// // الإرسال للذكاء الاصطناعي
|
||||
// await sendToAI(type, imageFile: outFile);
|
||||
} catch (e) {
|
||||
mySnackbarError('${'An unexpected error occurred:'.tr} $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ثابت: 20 لون سيارة شائع
|
||||
static const List<Map<String, String>> kCarColorOptions = [
|
||||
{'key': 'color.white', 'hex': '#FFFFFF'},
|
||||
{'key': 'color.black', 'hex': '#000000'},
|
||||
{'key': 'color.silver', 'hex': '#C0C0C0'},
|
||||
{'key': 'color.gray', 'hex': '#808080'},
|
||||
{'key': 'color.gunmetal', 'hex': '#2A3439'},
|
||||
{'key': 'color.red', 'hex': '#C62828'},
|
||||
{'key': 'color.blue', 'hex': '#1565C0'},
|
||||
{'key': 'color.navy', 'hex': '#0D47A1'},
|
||||
{'key': 'color.green', 'hex': '#2E7D32'},
|
||||
{'key': 'color.darkGreen', 'hex': '#1B5E20'},
|
||||
{'key': 'color.beige', 'hex': '#D7CCC8'},
|
||||
{'key': 'color.brown', 'hex': '#5D4037'},
|
||||
{'key': 'color.maroon', 'hex': '#800000'},
|
||||
{'key': 'color.burgundy', 'hex': '#800020'},
|
||||
{'key': 'color.yellow', 'hex': '#F9A825'},
|
||||
{'key': 'color.orange', 'hex': '#EF6C00'},
|
||||
{'key': 'color.gold', 'hex': '#D4AF37'},
|
||||
{'key': 'color.bronze', 'hex': '#CD7F32'},
|
||||
{'key': 'color.champagne', 'hex': '#EFE1C6'},
|
||||
{'key': 'color.purple', 'hex': '#6A1B9A'},
|
||||
];
|
||||
|
||||
Color hexToColor(String hex) {
|
||||
var v = hex.replaceAll('#', '');
|
||||
if (v.length == 6) v = 'FF$v';
|
||||
return Color(int.parse(v, radix: 16));
|
||||
}
|
||||
|
||||
//uploadSyrianDocs
|
||||
// دالة مساعدة: تضيف الحقل إذا كان له قيمة
|
||||
void _addField(Map<String, String> fields, String key, String? value) {
|
||||
if (value != null && value.toString().isNotEmpty) {
|
||||
fields[key] = value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// خريطة لتخزين روابط المستندات بعد الرفع
|
||||
final Map<String, String> docUrls = {
|
||||
'id_front': '',
|
||||
'id_back': '',
|
||||
'driver_license': '',
|
||||
'driver_license_back': '',
|
||||
'profile_picture': '',
|
||||
'criminal_record': '',
|
||||
'car_license_front': '',
|
||||
'car_license_back': '',
|
||||
};
|
||||
|
||||
String getCriminalRecordTitle() {
|
||||
String currentCountry = box.read(BoxName.countryCode) ?? 'Jordan';
|
||||
if (currentCountry == 'Syria') return 'لا حكم عليه'.tr;
|
||||
if (currentCountry == 'Egypt') return 'فيش وتشبيه'.tr;
|
||||
return 'عدم محكومية'.tr;
|
||||
}
|
||||
|
||||
/// التصرّف العام لاختيار/قص/ضغط/رفع الصورة حسب type
|
||||
Future<void> choosImage(String link, String imageType, {String? backupLink}) async {
|
||||
if (isloading) return;
|
||||
try {
|
||||
isloading = true;
|
||||
update();
|
||||
|
||||
final pickedImage = await picker.pickImage(
|
||||
source: ImageSource.camera,
|
||||
preferredCameraDevice: CameraDevice.rear,
|
||||
);
|
||||
if (pickedImage == null) {
|
||||
isloading = false;
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
image = File(pickedImage.path);
|
||||
|
||||
final 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 (croppedFile == null) {
|
||||
isloading = false;
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
// صورة للمعاينة داخل التطبيق
|
||||
myImage = File(croppedFile.path);
|
||||
|
||||
// ضغط (وأيضاً يمكنك إضافة rotateImageIfNeeded قبل/بعد الضغط إن رغبت)
|
||||
final File compressedImage = await compressImage(File(croppedFile.path));
|
||||
|
||||
// تجهيز الحقول
|
||||
final driverId = box.read(BoxName.driverID);
|
||||
|
||||
final payload = <String, String>{
|
||||
'driverID': driverId?.toString() ?? '',
|
||||
'imageType': imageType,
|
||||
'country': box.read(BoxName.countryCode) ?? '',
|
||||
};
|
||||
|
||||
// الرفع وإرجاع الرابط (يحاول الأساسي ثم الاحتياطي)
|
||||
final String imageUrl =
|
||||
await uploadImage(compressedImage, payload, link, backupLink: backupLink);
|
||||
|
||||
// حفظ الرابط محلياً حسب النوع
|
||||
docUrls[imageType] = imageUrl;
|
||||
|
||||
Log.print('✅ Uploaded $imageType => $imageUrl');
|
||||
} catch (e, st) {
|
||||
Log.print('❌ Error in choosImage: $e\n$st');
|
||||
mySnackbarError('Image Upload Failed'.tr);
|
||||
} finally {
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
/// ترفع الملف وترجع رابط الصورة النهائي كـ String مع إعادة المحاولة.
|
||||
/// يحاول الرابط الأساسي first، فإن فشل جرب الاحتياطي (إن وُجد).
|
||||
Future<String> uploadImage(
|
||||
File file,
|
||||
Map<String, String> data,
|
||||
String link, {
|
||||
String? backupLink,
|
||||
}) async {
|
||||
final List<String> urlsToTry = [link, if (backupLink != null && backupLink.trim().isNotEmpty) backupLink];
|
||||
|
||||
for (int i = 0; i < urlsToTry.length; i++) {
|
||||
final currentUrl = urlsToTry[i];
|
||||
final maxRetries = (i == 0) ? 3 : 1; // 3 attempts for primary, 1 for backup
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
final uri = Uri.parse(currentUrl);
|
||||
final request = http.MultipartRequest('POST', uri);
|
||||
|
||||
final _jwt = box.read(BoxName.jwt);
|
||||
final String _token = _jwt != null ? r(_jwt).split(AppInformation.addd)[0] : '';
|
||||
|
||||
String timestamp = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
String nonce = timestamp;
|
||||
|
||||
final headers = <String, String>{
|
||||
'Authorization': 'Bearer $_token',
|
||||
'X-Device-FP': box.read(BoxName.deviceFingerprint)?.toString() ?? '',
|
||||
'X-Timestamp': timestamp,
|
||||
'X-Nonce': nonce,
|
||||
};
|
||||
request.headers.addAll(headers);
|
||||
|
||||
final forcedName = '${box.read(BoxName.driverID) ?? 'image'}.jpg';
|
||||
|
||||
request.files.add(
|
||||
await http.MultipartFile.fromPath(
|
||||
'image',
|
||||
file.path,
|
||||
filename: forcedName,
|
||||
),
|
||||
);
|
||||
|
||||
data.forEach((k, v) => request.fields[k] = v);
|
||||
|
||||
final streamed =
|
||||
await request.send().timeout(const Duration(seconds: 120));
|
||||
final res = await http.Response.fromStream(streamed);
|
||||
|
||||
if (res.statusCode != 200) {
|
||||
throw Exception(
|
||||
'Failed to upload image: ${res.statusCode} - ${res.body}');
|
||||
}
|
||||
|
||||
final body = jsonDecode(res.body);
|
||||
String? url;
|
||||
// بعض السيرفرات تعيد الرابط داخل message
|
||||
if (body['message'] is Map) {
|
||||
url = body['message']['url'] ??
|
||||
body['message']['file_link'] ??
|
||||
body['message']['image_url'];
|
||||
}
|
||||
// وإلا حاول من الجذر
|
||||
url ??= body['url'] ??
|
||||
body['file_link'] ??
|
||||
body['image_url'] ??
|
||||
(body['data'] is Map ? body['data']['url'] : null);
|
||||
|
||||
if (url == null || url.isEmpty) {
|
||||
throw Exception(
|
||||
'Upload succeeded but no image URL found in response: ${res.body}');
|
||||
}
|
||||
|
||||
return url;
|
||||
} catch (e) {
|
||||
Log.print("⚠️ [Upload Attempt $attempt/$maxRetries @ $currentUrl] Error: $e");
|
||||
if (attempt >= maxRetries) {
|
||||
if (urlsToTry.last == currentUrl) {
|
||||
rethrow;
|
||||
}
|
||||
Log.print("➡️ Switching to backup URL...");
|
||||
} else {
|
||||
await Future.delayed(Duration(seconds: attempt * 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw Exception('Upload failed after all attempts');
|
||||
}
|
||||
|
||||
Future<File> compressImage(File file) async {
|
||||
final dir = await path_provider.getTemporaryDirectory();
|
||||
final targetPath = "${dir.absolute.path}/temp.jpg";
|
||||
|
||||
var result = await FlutterImageCompress.compressAndGetFile(
|
||||
file.absolute.path,
|
||||
targetPath,
|
||||
quality: 70,
|
||||
minWidth: 1024,
|
||||
minHeight: 1024,
|
||||
);
|
||||
|
||||
return File(result!.path);
|
||||
}
|
||||
|
||||
// دالة رفع إلى السيرفر السوري مع مهلة 120 ثانية وإعادة محاولة تلقائية في حال الفشل
|
||||
Future<String> uploadToSyria({
|
||||
required String docType,
|
||||
required File file,
|
||||
required Uri syrianUploadUri,
|
||||
required String authHeader,
|
||||
required String hmacHeader,
|
||||
required String driverId,
|
||||
Duration timeout = const Duration(seconds: 120),
|
||||
http.Client? clientOverride,
|
||||
}) async {
|
||||
int maxRetries = 3;
|
||||
int attempt = 0;
|
||||
while (attempt < maxRetries) {
|
||||
attempt++;
|
||||
final client = clientOverride ?? http.Client();
|
||||
try {
|
||||
final mime = lookupMimeType(file.path) ?? 'image/jpeg';
|
||||
final parts = mime.split('/');
|
||||
|
||||
final req = http.MultipartRequest('POST', syrianUploadUri);
|
||||
req.headers.addAll({
|
||||
'Authorization': authHeader,
|
||||
'X-Device-FP': box.read(BoxName.deviceFingerprint)?.toString() ?? '',
|
||||
});
|
||||
|
||||
req.fields['driver_id'] = driverId;
|
||||
req.fields['doc_type'] = docType;
|
||||
|
||||
req.files.add(
|
||||
await http.MultipartFile.fromPath(
|
||||
'file',
|
||||
file.path,
|
||||
filename: p.basename(file.path),
|
||||
contentType: MediaType(parts.first, parts.last),
|
||||
),
|
||||
);
|
||||
|
||||
final streamed = await client.send(req).timeout(timeout);
|
||||
final resp = await http.Response.fromStream(streamed);
|
||||
|
||||
Log.print('--- Syrian Upload Response (Attempt $attempt) ---');
|
||||
Log.print('Status: ${resp.statusCode}');
|
||||
|
||||
Map<String, dynamic> j = {};
|
||||
try {
|
||||
j = jsonDecode(resp.body) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
Log.print('⚠️ Failed to parse JSON: $e');
|
||||
}
|
||||
|
||||
final statusOk = j['status'] == 'success';
|
||||
final fileUrl =
|
||||
(j['file_url'] ?? j['message']?['file_url'])?.toString();
|
||||
|
||||
if (resp.statusCode == 200 &&
|
||||
statusOk &&
|
||||
(fileUrl?.isNotEmpty ?? false)) {
|
||||
return fileUrl!;
|
||||
}
|
||||
|
||||
throw Exception(
|
||||
'❌ Syrian upload failed ($docType): ${j['message'] ?? resp.body}');
|
||||
} catch (e) {
|
||||
Log.print("⚠️ [Syria Upload Attempt $attempt Failed] Error: $e");
|
||||
if (attempt >= maxRetries) {
|
||||
rethrow;
|
||||
}
|
||||
await Future.delayed(Duration(seconds: attempt * 2));
|
||||
} finally {
|
||||
if (clientOverride == null) client.close();
|
||||
}
|
||||
}
|
||||
throw Exception('Syrian upload failed after $maxRetries attempts');
|
||||
}
|
||||
|
||||
Future<void> submitRegistration() async {
|
||||
// 0) دوال/مساعدات محلية
|
||||
void _addField(Map<String, String> fields, String key, String? value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
fields[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 1) تحقق من وجود الروابط
|
||||
final idFrontUrl = docUrls['id_front'];
|
||||
final idBackUrl = docUrls['id_back'];
|
||||
final driverLicenseUrl = docUrls['driver_license'];
|
||||
final driverLicenseBackUrl = docUrls['driver_license_back'];
|
||||
final profilePicUrl = docUrls['profile_picture'];
|
||||
final criminalRecordUrl = docUrls['criminal_record'];
|
||||
final carFrontUrl = docUrls['car_license_front'];
|
||||
final carBackUrl = docUrls['car_license_back'];
|
||||
|
||||
final isSyria = box.read(BoxName.countryCode) == 'Syria';
|
||||
|
||||
if (idFrontUrl == null ||
|
||||
idFrontUrl.isEmpty ||
|
||||
idBackUrl == null ||
|
||||
idBackUrl.isEmpty ||
|
||||
driverLicenseUrl == null ||
|
||||
driverLicenseUrl.isEmpty ||
|
||||
(isSyria &&
|
||||
(driverLicenseBackUrl == null || driverLicenseBackUrl.isEmpty)) ||
|
||||
profilePicUrl == null ||
|
||||
profilePicUrl.isEmpty ||
|
||||
carFrontUrl == null ||
|
||||
carFrontUrl.isEmpty ||
|
||||
carBackUrl == null ||
|
||||
carBackUrl.isEmpty) {
|
||||
mySnackbarWarning(
|
||||
'Please wait for all documents to finish uploading before registering.'
|
||||
.tr);
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
update();
|
||||
|
||||
final registerUri = Uri.parse(AppLink.register_driver_and_car);
|
||||
final client = http.Client();
|
||||
|
||||
try {
|
||||
// ترويسات مشتركة
|
||||
final _jwt = box.read(BoxName.jwt);
|
||||
final bearer = 'Bearer ${_jwt != null ? r(_jwt).split(AppInformation.addd)[0] : ''}';
|
||||
final hmac = '${box.read(BoxName.hmac)}';
|
||||
|
||||
String fingerPrint =
|
||||
box.read(BoxName.deviceFingerprint)?.toString() ?? '';
|
||||
String timestamp = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
String nonce = timestamp; // Simple nonce for now
|
||||
|
||||
final req = http.MultipartRequest('POST', registerUri);
|
||||
req.headers.addAll({
|
||||
'Authorization': bearer,
|
||||
// 'X-HMAC-Auth': hmac, // Removed to bypass "Invalid HMAC signature" check
|
||||
'X-Device-FP': fingerPrint,
|
||||
'X-Timestamp': timestamp,
|
||||
'X-Nonce': nonce,
|
||||
});
|
||||
|
||||
final fields = <String, String>{};
|
||||
|
||||
// --- Driver Data ---
|
||||
_addField(fields, 'id', box.read(BoxName.driverID)?.toString());
|
||||
_addField(fields, 'first_name', firstNameController.text);
|
||||
_addField(fields, 'last_name', lastNameController.text);
|
||||
_addField(fields, 'phone', box.read(BoxName.phoneDriver) ?? '');
|
||||
_addField(fields, 'national_number', nationalIdController.text);
|
||||
_addField(fields, 'birthdate', bithdateController.text);
|
||||
_addField(fields, 'expiry_date', driverLicenseExpiryController.text);
|
||||
_addField(fields, 'password', 'generated_password_or_token');
|
||||
_addField(fields, 'status', 'yet');
|
||||
|
||||
String generatedEmail = 'driver_${box.read(BoxName.driverID) ?? DateTime.now().millisecondsSinceEpoch}@siromove.com';
|
||||
_addField(fields, 'email', generatedEmail);
|
||||
_addField(fields, 'gender', 'Male'); // يفضل ربطها بـ Dropdown أيضاً
|
||||
_addField(fields, 'country', box.read(BoxName.countryCode)?.toString() ?? 'Jordan');
|
||||
_addField(fields, 'site', siteController.text.isNotEmpty ? siteController.text : null);
|
||||
|
||||
// --- Car Data ---
|
||||
_addField(fields, 'vin', carVinController.text.isNotEmpty ? carVinController.text : 'yet');
|
||||
_addField(fields, 'car_plate', carPlateController.text);
|
||||
_addField(fields, 'make', carMakeController.text);
|
||||
_addField(fields, 'model', carModelController.text);
|
||||
_addField(fields, 'year', carYearController.text);
|
||||
_addField(
|
||||
fields,
|
||||
'expiration_date',
|
||||
carRegistrationExpiryController.text.isNotEmpty
|
||||
? carRegistrationExpiryController.text
|
||||
: driverLicenseExpiryController.text);
|
||||
_addField(
|
||||
fields,
|
||||
'color',
|
||||
carColorController.text.isNotEmpty
|
||||
? carColorController.text
|
||||
: 'White');
|
||||
|
||||
_addField(fields, 'color_hex',
|
||||
(colorHex != null && colorHex!.isNotEmpty) ? colorHex! : '#FFFFFF');
|
||||
|
||||
_addField(
|
||||
fields,
|
||||
'owner',
|
||||
'${firstNameController.text} ${lastNameController.text}'
|
||||
.trim()
|
||||
.isNotEmpty
|
||||
? '${firstNameController.text} ${lastNameController.text}'
|
||||
: 'Driver Owner');
|
||||
|
||||
// ============================================================
|
||||
// 🔥 التعديل الجديد: إرسال الأرقام (IDs) لتصنيف المركبة والوقود
|
||||
// ============================================================
|
||||
|
||||
// 1. إرسال رقم تصنيف المركبة (1=سيارة, 2=دراجة...)
|
||||
if (selectedVehicleCategoryId != null) {
|
||||
_addField(fields, 'vehicle_category_id',
|
||||
selectedVehicleCategoryId.toString());
|
||||
} else {
|
||||
_addField(fields, 'vehicle_category_id', '1'); // قيمة افتراضية (سيارة)
|
||||
}
|
||||
|
||||
// 2. إرسال رقم ونوع الوقود
|
||||
if (selectedFuelTypeId != null) {
|
||||
// إرسال الرقم (للبحث السريع)
|
||||
_addField(fields, 'fuel_type_id', selectedFuelTypeId.toString());
|
||||
|
||||
// إرسال الاسم نصاً (للتوافق مع العمود القديم 'fuel' إذا لزم الأمر)
|
||||
// نبحث عن الاسم داخل القائمة بناءً على الرقم المختار
|
||||
final fuelObj = fuelTypeOptions.firstWhere(
|
||||
(e) => e['id'] == selectedFuelTypeId,
|
||||
orElse: () => {'name': 'Petrol'});
|
||||
_addField(fields, 'fuel', fuelObj['name'].toString());
|
||||
} else {
|
||||
_addField(fields, 'fuel_type_id', '1');
|
||||
_addField(fields, 'fuel', 'Petrol');
|
||||
}
|
||||
|
||||
// --- روابط الصور ---
|
||||
_addField(fields, 'id_front', idFrontUrl);
|
||||
_addField(fields, 'id_back', idBackUrl);
|
||||
_addField(fields, 'driver_license', driverLicenseUrl);
|
||||
// Always add the key to avoid PHP undefined index error
|
||||
fields['driver_license_back'] = driverLicenseBackUrl ?? '';
|
||||
_addField(fields, 'profile_picture', profilePicUrl);
|
||||
_addField(fields, 'criminal_record', criminalRecordUrl);
|
||||
_addField(fields, 'car_license_front', carFrontUrl);
|
||||
_addField(fields, 'car_license_back', carBackUrl);
|
||||
|
||||
req.fields.addAll(fields);
|
||||
|
||||
Log.print('--- 🚀 Registration Request Payload ---');
|
||||
req.fields.forEach((key, value) {
|
||||
Log.print('[$key]: $value');
|
||||
});
|
||||
Log.print('---------------------------------------');
|
||||
|
||||
// 3) الإرسال
|
||||
final streamed =
|
||||
await client.send(req).timeout(const Duration(seconds: 60));
|
||||
final resp = await http.Response.fromStream(streamed);
|
||||
|
||||
// 4) معالجة الاستجابة
|
||||
Map<String, dynamic>? json;
|
||||
try {
|
||||
Log.print('--- Registration Response: ${resp.body} ---');
|
||||
json = jsonDecode(resp.body) as Map<String, dynamic>;
|
||||
} catch (_) {}
|
||||
|
||||
if (resp.statusCode == 200 && json?['status'] == 'success') {
|
||||
mySnackbarSuccess('Registration completed successfully!'.tr);
|
||||
|
||||
// منطق التوكن والإشعارات وتسجيل الدخول...
|
||||
final email = box.read(BoxName.emailDriver);
|
||||
final driverID = box.read(BoxName.driverID);
|
||||
|
||||
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
||||
await CRUD().post(link: AppLink.addTokensDriver, payload: {
|
||||
'captain_id': (box.read(BoxName.driverID)).toString(),
|
||||
'token': (box.read(BoxName.tokenDriver)).toString(),
|
||||
'fingerPrint': fingerPrint.toString(),
|
||||
});
|
||||
|
||||
NotificationService.sendNotification(
|
||||
target: 'service',
|
||||
title: 'New Driver Registration',
|
||||
body: 'Driver $driverID has submitted registration.',
|
||||
isTopic: true,
|
||||
category: 'new_service_request',
|
||||
);
|
||||
|
||||
final c = Get.isRegistered<LoginDriverController>()
|
||||
? Get.find<LoginDriverController>()
|
||||
: Get.put(LoginDriverController());
|
||||
box.write(BoxName.firstTimeLoadKey, 'false');
|
||||
await c.getJWT();
|
||||
c.loginDriver(driverID, email);
|
||||
} else {
|
||||
final msg = (json?['message'] ?? 'Registration failed.').toString();
|
||||
mySnackbarError(msg);
|
||||
}
|
||||
} catch (e) {
|
||||
mySnackbarError('Error: $e');
|
||||
} finally {
|
||||
client.close();
|
||||
isLoading.value = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
// // 1) تحقق من الصور
|
||||
// if (driverLicenseFrontImage == null ||
|
||||
// driverLicenseBackImage == null ||
|
||||
// carLicenseFrontImage == null ||
|
||||
// carLicenseBackImage == null) {
|
||||
// Get.snackbar(
|
||||
// 'Missing Documents'.tr, 'Please upload all 4 required documents.'.tr,
|
||||
// snackPosition: SnackPosition.BOTTOM,
|
||||
// backgroundColor: Colors.orange,
|
||||
// colorText: Colors.white);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// isLoading.value = true;
|
||||
|
||||
// final uri = Uri.parse(
|
||||
// 'https://intaleq.xyz/siro/auth/driver/register.php',
|
||||
// );
|
||||
|
||||
// final client = http.Client();
|
||||
// try {
|
||||
// final req = http.MultipartRequest('POST', uri);
|
||||
|
||||
// // مهم: لا تضع Content-Type يدويًا، الـ MultipartRequest يتكفّل فيه ببناء boundary.
|
||||
// final headers = {
|
||||
// 'Authorization':
|
||||
// 'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}',
|
||||
// 'X-HMAC-Auth': '${box.read(BoxName.hmac)}',
|
||||
// };
|
||||
// // 2) الحقول النصية
|
||||
// final fields = <String, String>{};
|
||||
|
||||
// // --- Driver Data ---
|
||||
// _addField(fields, 'id', box.read(BoxName.driverID)?.toString());
|
||||
// _addField(fields, 'first_name', firstNameController.text);
|
||||
// _addField(fields, 'last_name', lastNameController.text);
|
||||
// _addField(fields, 'phone', box.read(BoxName.phoneDriver) ?? '');
|
||||
// _addField(fields, 'national_number', nationalIdController.text);
|
||||
// _addField(fields, 'expiry_date', driverLicenseExpiryController.text);
|
||||
// _addField(
|
||||
// fields, 'password', 'generate_your_password_here'); // عدّل حسب منطقك
|
||||
// _addField(fields, 'status', 'yet');
|
||||
// _addField(fields, 'email',
|
||||
// 'Not specified'); // سكربت السيرفر سيحوّلها null ويبني ايميل افتراضي
|
||||
// _addField(fields, 'gender', 'Male');
|
||||
|
||||
// // --- Car Data (مطابقة لما يتوقّعه السكربت) ---
|
||||
// _addField(fields, 'vin', 'carVinController.text);');
|
||||
// _addField(fields, 'car_plate', carPlateController.text);
|
||||
// _addField(fields, 'make', carMakeController.text);
|
||||
// _addField(fields, 'model', carModelController.text);
|
||||
// _addField(fields, 'year', carYearController.text);
|
||||
// _addField(fields, 'expiration_date', 'carRegistrationExpiryController');
|
||||
// _addField(fields, 'color', carColorController.text);
|
||||
// _addField(fields, 'fuel', 'Gasoline'); // أو حسب اختيارك
|
||||
// _addField(fields, 'color_hex', colorHex); // مهم
|
||||
// // لو عندك حقول إضافية مطلوبة بالسكربت (مالك المركبة / الكود اللوني / الوقود) مرّرها:
|
||||
// _addField(fields, 'owner',
|
||||
// firstNameController.text + ' ' + lastNameController.text);
|
||||
// // if (colorHex != null) _addField(fields, 'color_hex', colorHex);
|
||||
// // if (fuelType != null) _addField(fields, 'fuel', fuelType);
|
||||
// req.headers.addAll(headers);
|
||||
// req.fields.addAll(fields);
|
||||
|
||||
// // 3) الملفات (4 صور) — مفاتيحها مطابقة للسكربت
|
||||
// Future<void> addFile(String field, File file) async {
|
||||
// final mime = lookupMimeType(file.path) ?? 'image/jpeg';
|
||||
// final parts = mime.split('/');
|
||||
// final mediaType = MediaType(parts.first, parts.last);
|
||||
// req.files.add(
|
||||
// await http.MultipartFile.fromPath(
|
||||
// field,
|
||||
// file.path,
|
||||
// filename: p.basename(file.path),
|
||||
// contentType: mediaType,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
// await addFile('driver_license_front', driverLicenseFrontImage!);
|
||||
// await addFile('driver_license_back', driverLicenseBackImage!);
|
||||
// await addFile('car_license_front', carLicenseFrontImage!);
|
||||
// await addFile('car_license_back', carLicenseBackImage!);
|
||||
|
||||
// // 4) الإرسال
|
||||
// final streamed =
|
||||
// await client.send(req).timeout(const Duration(seconds: 60));
|
||||
// final resp = await http.Response.fromStream(streamed);
|
||||
|
||||
// // 5) فحص النتيجة
|
||||
// Map<String, dynamic>? json;
|
||||
// try {
|
||||
// json = jsonDecode(resp.body) as Map<String, dynamic>;
|
||||
// } catch (_) {}
|
||||
|
||||
// if (resp.statusCode == 200 &&
|
||||
// json != null &&
|
||||
// json['status'] == 'success') {
|
||||
// // ممكن يرجّع driverID, carRegID, documents
|
||||
// final driverID =
|
||||
// (json['data']?['driverID'] ?? json['driverID'])?.toString();
|
||||
// if (driverID != null && driverID.isNotEmpty) {
|
||||
// box.write(BoxName.driverID, driverID);
|
||||
// }
|
||||
|
||||
// Get.snackbar('Success'.tr, 'Registration completed successfully!'.tr,
|
||||
// snackPosition: SnackPosition.BOTTOM,
|
||||
// backgroundColor: Colors.green,
|
||||
// colorText: Colors.white);
|
||||
|
||||
// // TODO: انتقل للصفحة التالية أو حدّث الحالة…
|
||||
// } else {
|
||||
// final msg =
|
||||
// (json?['message'] ?? 'Registration failed. Please try again.')
|
||||
// .toString();
|
||||
// Get.snackbar('Error'.tr, msg,
|
||||
// snackPosition: SnackPosition.BOTTOM,
|
||||
// backgroundColor: Colors.red,
|
||||
// colorText: Colors.white);
|
||||
// }
|
||||
// } catch (e) {
|
||||
// Get.snackbar('Error'.tr, '${'An unexpected error occurred:'.tr} $e',
|
||||
// snackPosition: SnackPosition.BOTTOM,
|
||||
// backgroundColor: Colors.red,
|
||||
// colorText: Colors.white);
|
||||
// } finally {
|
||||
// client.close();
|
||||
// isLoading.value = false;
|
||||
// }
|
||||
// }
|
||||
|
||||
// Helpers
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
/// Syria Driver Registration Simulation
|
||||
///
|
||||
/// Traces the complete flow:
|
||||
/// 1. Phone OTP Verification
|
||||
/// 2. Personal + Car Info Input
|
||||
/// 3. Document Image Capture (license, ID, car docs)
|
||||
/// 4. AI/OCR Processing (per-document via ai_document.php)
|
||||
/// 5. Document Upload (uploadSyrianDocs.php → signed URL)
|
||||
/// 6. Final Submission (register_driver_and_car.php → Gemini bulk AI → DB insert → FCM)
|
||||
/// 7. Admin Review & Activation
|
||||
///
|
||||
/// Run: `dart run siro_driver/lib/controller/auth/syria/registration_simulation.dart`
|
||||
|
||||
void main() async {
|
||||
print('═══════════════════════════════════════════════════════');
|
||||
print(' SYRIA DRIVER REGISTRATION SIMULATION');
|
||||
print('═══════════════════════════════════════════════════════');
|
||||
|
||||
const host = 'api-syria.siromove.com';
|
||||
const protocol = 'https';
|
||||
const driverId = 'DRV2026061912345678';
|
||||
|
||||
// ================================================================
|
||||
// STEP 1: Initial Phone OTP Verification
|
||||
// ================================================================
|
||||
print('\n─── STEP 1: Phone OTP Verification ───');
|
||||
const phone = '+963992952235';
|
||||
print(' Phone: $phone');
|
||||
|
||||
// Backend: POST /auth/otp/request.php
|
||||
// Payload: { phone_number, purpose: "driver_registration" }
|
||||
// Checks: last_request < 120s → cooldown
|
||||
// Inserts/updates: phone_verification(phone_number, code, is_verified=0, last_request)
|
||||
// Sends SMS with code
|
||||
print(' → POST /auth/otp/request.php');
|
||||
print(' Request: { phone_number: "$phone", purpose: "driver_registration" }');
|
||||
print(' ✅ OTP sent (cooldown 120s applied)');
|
||||
|
||||
// Backend: POST /auth/otp/verify.php
|
||||
// Payload: { phone_number, code, device_token }
|
||||
// Validates: code match, within TTL
|
||||
// Sets: phone_verification.is_verified = 1
|
||||
// Returns: driver_id (new or existing)
|
||||
print(' → POST /auth/otp/verify.php');
|
||||
print(' Request: { phone_number: "$phone", code: "123456", device_token: "fp_abc123" }');
|
||||
print(' ✅ Phone verified, driver_id returned');
|
||||
|
||||
// Check: GET /auth/driver/isPhoneVerified.php
|
||||
// Query: phone_number
|
||||
// Returns: success if is_verified = 1
|
||||
print(' → GET /auth/driver/isPhoneVerified.php?phone_number=$phone');
|
||||
print(' ✅ Phone is verified, proceeding to registration');
|
||||
|
||||
// ================================================================
|
||||
// STEP 2: Personal Information Input (Flutter UI)
|
||||
// ================================================================
|
||||
print('\n─── STEP 2: Personal Information ───');
|
||||
final driverInfo = {
|
||||
'first_name': 'أحمد',
|
||||
'last_name': 'الأسد',
|
||||
'national_number': '04020123456',
|
||||
'birthdate': '1990-05-15',
|
||||
'phone': phone,
|
||||
'driver_license_expiry': '2028-06-01',
|
||||
};
|
||||
print(' Driver info collected:');
|
||||
driverInfo.forEach((k, v) => print(' $k: $v'));
|
||||
|
||||
// ================================================================
|
||||
// STEP 3: Car Information Input (Flutter UI)
|
||||
// ================================================================
|
||||
print('\n─── STEP 3: Car Information ───');
|
||||
final carInfo = {
|
||||
'car_plate': '155186 درعا',
|
||||
'make': 'Hyundai',
|
||||
'model': 'H1',
|
||||
'year': '2019',
|
||||
'color': 'White',
|
||||
'color_hex': '#FFFFFF',
|
||||
'vehicle_category_id': 1, // 1=Car, 2=Motorcycle, 3=Van/Bus
|
||||
'fuel_type_id': 1, // 1=Petrol, 2=Diesel, 3=Electric, 4=Hybrid
|
||||
'owner': 'أحمد الأسد',
|
||||
'vin': 'KMHSH81XSBU123456',
|
||||
};
|
||||
print(' Car info collected:');
|
||||
carInfo.forEach((k, v) => print(' $k: $v'));
|
||||
|
||||
// ================================================================
|
||||
// STEP 4: Document Image Capture & Per-Document AI/OCR
|
||||
// ================================================================
|
||||
print('\n─── STEP 4: Document Image Capture + AI/OCR ───');
|
||||
|
||||
final documents = [
|
||||
'id_front_sy',
|
||||
'id_back_sy',
|
||||
'driving_license_sy_front',
|
||||
'driving_license_sy_back',
|
||||
'vehicle_license_sy_front',
|
||||
'vehicle_license_sy_back',
|
||||
'profile_picture',
|
||||
'criminal_record',
|
||||
];
|
||||
|
||||
for (final docType in documents) {
|
||||
print(' 📷 Capturing: $docType');
|
||||
print(' → ImagePicker.camera → ImageCropper → FlutterImageCompress');
|
||||
print(' → POST /auth/document_syria/ai_document.php');
|
||||
print(' Fields: driver_id, type=$docType, image (multipart)');
|
||||
print(' AI model: gemini-flash-lite-latest (per-document OCR)');
|
||||
|
||||
// Each document type has a unique Gemini prompt
|
||||
String expectedExtraction;
|
||||
switch (docType) {
|
||||
case 'id_front_sy':
|
||||
expectedExtraction = '{ "full_name": "أحمد الأسد", "national_number": "04020123456", "dob": "1990-05-15", "address": "دمشق" }';
|
||||
break;
|
||||
case 'id_back_sy':
|
||||
expectedExtraction = '{ "governorate": "دمشق", "address": "المزة", "gender": "Male", "issue_date": "2010-03-01" }';
|
||||
break;
|
||||
case 'driving_license_sy_front':
|
||||
expectedExtraction = '{ "name_arabic": "أحمد الأسد", "birth_place": "دمشق", "birth_year": "1990", "national_number": "04020123456", "civil_registry": "دمشق 3", "blood_type": "A+" }';
|
||||
break;
|
||||
case 'driving_license_sy_back':
|
||||
expectedExtraction = '{ "issue_date": "2018-06-15", "expiry_date": "2028-06-15", "license_number": "987654", "license_category": "D1" }';
|
||||
break;
|
||||
case 'vehicle_license_sy_front':
|
||||
expectedExtraction = '{ "car_plate": "155186 درعا", "owner": "أحمد الأسد", "vin": "KMHSH81XSBU123456", "color": "White", "color_hex": "#FFFFFF", "issue_date": "2019-03-10", "inspection_date": "2025-03-10" }';
|
||||
break;
|
||||
case 'vehicle_license_sy_back':
|
||||
expectedExtraction = '{ "make": "Hyundai", "model": "H1", "year": "2019", "fuel": "Petrol", "chassis": "KMHSH81XSBU123456" }';
|
||||
break;
|
||||
default:
|
||||
expectedExtraction = '{ "status": "success" }';
|
||||
}
|
||||
print(' ✅ AI extraction: $expectedExtraction');
|
||||
|
||||
print(' ✅ Image URL: $protocol://$host/siro/auth/uploads/documents/driver_${docType}_$driverId.jpg');
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// STEP 5: Document Upload to Secure Storage
|
||||
// ================================================================
|
||||
print('\n─── STEP 5: Document Upload (Secure Signed URLs) ───');
|
||||
|
||||
final docUrls = <String, String>{};
|
||||
const uploadDocTypes = [
|
||||
'driver_license_front',
|
||||
'driver_license_back',
|
||||
'car_license_front',
|
||||
'car_license_back',
|
||||
];
|
||||
|
||||
|
||||
|
||||
for (final docType in uploadDocTypes) {
|
||||
print(' 📤 Uploading: $docType');
|
||||
print(' → POST /auth/driver/uploadDriverDocs.php');
|
||||
print(' Fields: driver_id=$driverId, doc_type=$docType, file (multipart)');
|
||||
print(' File stored: private_uploads/<hash>/<driverId>__${docType}.jpg');
|
||||
print(' MIME validation: image/jpeg, image/png, image/webp');
|
||||
print(' Max size: 5 MB');
|
||||
|
||||
final signedUrl = '$protocol://$host/siro/secure_image.php'
|
||||
'?driver_id=$driverId'
|
||||
'&doc_type=$docType'
|
||||
'&ext=jpg'
|
||||
'&expires=1780000000'
|
||||
'&signature=abc123def456';
|
||||
|
||||
docUrls[docType] = signedUrl;
|
||||
print(' ✅ Signed URL: $signedUrl');
|
||||
print(' TTL: 2 days (172800s)');
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// STEP 6: Final Registration Submission
|
||||
// ================================================================
|
||||
print('\n─── STEP 6: Final Registration (register_driver_and_car.php) ───');
|
||||
print(' → POST /auth/driver/register.php');
|
||||
print(' This is the MAIN orchestrator. It does:');
|
||||
print(' 1. Phone number normalization (Syria/Jordan/Egypt logic)');
|
||||
print(' 2. Encrypt sensitive fields (phone, email, name, address, etc.)');
|
||||
print(' 3. HMAC-based password derivation from id|phone|national_number');
|
||||
print(' 4. Check for duplicate phone/email');
|
||||
print(' 5. Insert into `driver` table with status="yet"');
|
||||
print(' 6. Insert into `CarRegistration` with vehicle_category_id + fuel_type_id');
|
||||
print(' 7. Insert document links into `driver_documents` table');
|
||||
print(' 8. Store profile_picture into `imageProfileCaptain`');
|
||||
print(' 9. AI BULK ANALYSIS via Gemini Flash Lite (all docs together)');
|
||||
print(' 10. Send FCM notification → topic: new_driver_registration');
|
||||
print(' 11. Return { status: "success", driverID, carRegID, documents }');
|
||||
|
||||
// Show the full payload
|
||||
print('\n 📦 Request payload (multipart):');
|
||||
print(' Driver fields:');
|
||||
for (final f in ['id', 'first_name', 'last_name', 'phone', 'national_number', 'birthdate', 'status', 'gender', 'email']) {
|
||||
print(' $f: ${f == 'id' ? driverId : driverInfo[f] ?? carInfo[f] ?? '—'}');
|
||||
}
|
||||
print(' Car fields:');
|
||||
for (final f in ['vin', 'car_plate', 'make', 'model', 'year', 'color', 'color_hex', 'owner', 'vehicle_category_id', 'fuel_type_id']) {
|
||||
print(' $f: ${carInfo[f] ?? '—'}');
|
||||
}
|
||||
print(' Document URLs:');
|
||||
docUrls.forEach((k, v) => print(' $k: ${v.substring(0, 60)}...'));
|
||||
|
||||
// Simulate the transaction
|
||||
print('\n 🔄 Transaction: BEGIN');
|
||||
print(' 🔍 Check duplicate: SELECT id FROM driver WHERE phone = :p OR email = :e');
|
||||
print(' ✅ No duplicates found');
|
||||
print(' 📝 INSERT INTO driver (...) VALUES (...)');
|
||||
print(' → driverID: $driverId');
|
||||
print(' 📝 INSERT INTO CarRegistration (...) VALUES (...)');
|
||||
print(' → carRegID: 1');
|
||||
print(' 📝 INSERT INTO driver_documents (8 rows)');
|
||||
print(' 📝 INSERT INTO imageProfileCaptain (1 row)');
|
||||
print(' 🔄 Transaction: COMMIT');
|
||||
print(' ✅ Registration committed');
|
||||
|
||||
// AI Bulk Analysis
|
||||
print('\n 🤖 AI Bulk Analysis (Gemini Flash Lite):');
|
||||
print(' → Prompt includes all 8 document images + schema instructions');
|
||||
print(' → SSRF protection: only ALLOWED_UPLOAD_HOST URLs fetched');
|
||||
print(' → Extracted fields override user input:');
|
||||
print(' - full_name → first_name / last_name');
|
||||
print(' - national_number, dob, address, governorate → site');
|
||||
print(' - gender, license dates, car details');
|
||||
print(' ↳ If AI status="failure" → registration ABORTED with error message');
|
||||
print(' ✅ AI verification passed');
|
||||
|
||||
// FCM Notification
|
||||
print('\n 📲 FCM Notification:');
|
||||
print(' → POST /ride/firebase/send_fcm.php');
|
||||
print(' → Topic: new_driver_registration');
|
||||
print(' → Title: "تسجيل سائق جديد"');
|
||||
print(' → Body: "سائق جديد (أحمد الأسد) سجل برقم ID: $driverId وهو بانتظار المراجعة والتفعيل."');
|
||||
print(' ✅ Admin notified');
|
||||
|
||||
// Response
|
||||
print('\n 📤 Response:');
|
||||
print(' {');
|
||||
print(' "status": "success",');
|
||||
print(' "driverID": "$driverId",');
|
||||
print(' "carRegID": 1,');
|
||||
print(' "documents": {');
|
||||
docUrls.forEach((k, v) => print(' "$k": "$v",'));
|
||||
print(' }');
|
||||
print(' }');
|
||||
|
||||
// ================================================================
|
||||
// STEP 7: Admin Review & Activation
|
||||
// ================================================================
|
||||
print('\n─── STEP 7: Admin Review & Activation ───');
|
||||
|
||||
// Admin fetches pending drivers
|
||||
print(' 🔍 Admin fetches pending drivers:');
|
||||
print(' → GET /auth/driver/drivers_pending_list.php');
|
||||
print(' Decrypts: phone, first_name, last_name');
|
||||
print(' Returns: [{ id: "$driverId", first_name: "أحمد", last_name: "الأسد", phone: "$phone" }]');
|
||||
print(' ✅ Driver appears in pending list (status != "active")');
|
||||
|
||||
// Admin views details
|
||||
print('\n 📋 Admin views driver details:');
|
||||
print(' → GET /auth/driver/driver_details.php?driver_id=$driverId');
|
||||
print(' Returns: driver row + car registration + document links');
|
||||
print(' ✅ Admin reviews all documents');
|
||||
|
||||
// Admin activates driver
|
||||
print('\n ✅ Admin approves & activates:');
|
||||
print(' → POST /Admin/Staff/activate.php');
|
||||
print(' Auth: JWT (role=super_admin or admin)');
|
||||
print(' Payload: { user_id: "$driverId", type: "admin" }');
|
||||
print(' SQL: UPDATE driver SET status = "active" WHERE id = "$driverId"');
|
||||
|
||||
// Check driver_assurance table (references in migration schema)
|
||||
print('\n ℹ️ driver_assurance table (optional):');
|
||||
print(' May store health_insurance_provider + assured status');
|
||||
print(' Linked via driver_id (encrypted columns)');
|
||||
|
||||
// Final driver state
|
||||
print('\n 📊 Final driver state in DB:');
|
||||
print(' driver.phone = <encrypted>$phone');
|
||||
print(' driver.status = "active"');
|
||||
print(' driver.is_verified = 1 (via phone_verification)');
|
||||
print(' CarRegistration.status = "yet"');
|
||||
print(' driver_documents: 8 rows linked to driverID');
|
||||
|
||||
// ================================================================
|
||||
// SUMMARY
|
||||
// ================================================================
|
||||
print('\n═══════════════════════════════════════════════════════');
|
||||
print(' REGISTRATION FLOW COMPLETE');
|
||||
print('═══════════════════════════════════════════════════════');
|
||||
print(' Files involved:');
|
||||
print(' App (Flutter):');
|
||||
print(' - registration_controller.dart (multi-step UI)');
|
||||
print(' - register_captin_controller.dart (basic signup)');
|
||||
print(' - opt_token_controller.dart (OTP send/verify)');
|
||||
print(' - ml_google_doc.dart (phone OCR scanner)');
|
||||
print(' Backend (PHP):');
|
||||
print(' - /auth/otp/request.php + verify.php');
|
||||
print(' - /auth/driver/isPhoneVerified.php');
|
||||
print(' - /auth/document_syria/ai_document.php');
|
||||
print(' - /auth/driver/uploadDriverDocs.php');
|
||||
print(' - /auth/driver/register.php');
|
||||
print(' - /auth/driver/drivers_pending_list.php');
|
||||
print(' - /auth/driver/driver_details.php');
|
||||
print(' - /serviceapp/getDriversWaitingActive.php');
|
||||
print(' - /Admin/Staff/activate.php');
|
||||
print(' Schema:');
|
||||
print(' - schema_primary.sql (driver, CarRegistration, driver_documents,');
|
||||
print(' phone_verification, imageProfileCaptain,');
|
||||
print(' driver_gifts, driver_assurance)');
|
||||
print('═══════════════════════════════════════════════════════');
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../main.dart';
|
||||
|
||||
class TokenController extends GetxController {
|
||||
bool isloading = false;
|
||||
|
||||
Future addToken() async {
|
||||
String? basicAuthCredentials =
|
||||
await storage.read(key: BoxName.basicAuthCredentials);
|
||||
isloading = true;
|
||||
update();
|
||||
var res = await http.post(
|
||||
Uri.parse(AppLink.addTokens),
|
||||
headers: {
|
||||
'Authorization':
|
||||
'Basic ${base64Encode(utf8.encode(basicAuthCredentials.toString()))}',
|
||||
},
|
||||
body: {
|
||||
'token': box.read(BoxName.tokenFCM.toString()),
|
||||
'passengerID': box.read(BoxName.passengerID).toString()
|
||||
},
|
||||
);
|
||||
|
||||
isloading = false;
|
||||
update();
|
||||
var jsonToken = jsonDecode(res.body);
|
||||
if (jsonToken['status'] == 'The token has been updated successfully.') {
|
||||
mySnackbarSuccess('token updated'.tr);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user