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:sefer_driver/constant/box_name.dart'; import 'package:sefer_driver/constant/links.dart'; import 'package:sefer_driver/controller/functions/crud.dart'; import 'package:sefer_driver/controller/home/payment/captain_wallet_controller.dart'; import 'package:sefer_driver/main.dart'; import 'package:sefer_driver/views/widgets/error_snakbar.dart'; import 'package:sefer_driver/views/widgets/mydialoug.dart'; import 'package:share_plus/share_plus.dart'; import '../../firebase/local_notification.dart'; import '../../functions/launch.dart'; import '../../notification/notification_captain_controller.dart'; class InviteController extends GetxController { final TextEditingController invitePhoneController = TextEditingController(); List driverInvitationData = []; List driverInvitationDataToPassengers = []; String? couponCode; String? driverCouponCode; // **FIX**: Added the missing 'contacts' and 'contactMaps' definitions. List contacts = []; RxList> contactMaps = >[].obs; int selectedTab = 0; PassengerStats passengerStats = PassengerStats(); void updateSelectedTab(int index) { selectedTab = index; update(); } Future shareDriverCode() async { if (driverCouponCode != null) { final String shareText = '''Join Intaleq as a driver using my referral code! Use code: $driverCouponCode Download the Intaleq Driver app now and earn rewards! '''; await Share.share(shareText); } } Future sharePassengerCode() async { if (couponCode != null) { final String shareText = '''Get a discount on your first Intaleq ride! Use my referral code: $couponCode Download the Intaleq app now and enjoy your ride! '''; await Share.share(shareText); } } @override void onInit() { super.onInit(); // **MODIFIED**: Sync contacts automatically on controller initialization. syncContactsToServerOnce(); // fetchDriverStats(); } // --- NEW LOGIC: ONE-TIME CONTACTS SYNC --- /// **NEW**: Syncs all phone contacts to the server, but only runs once per user. Future 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 allContacts = await FlutterContacts.getContacts(withProperties: true); // **FIX**: Assign fetched contacts to the class variable. contacts = allContacts; contactMaps.value = contacts.map((contact) { return { 'name': contact.displayName, 'phones': contact.phones.map((phone) => phone.normalizedNumber).toList(), 'emails': contact.emails.map((email) => email.address).toList(), }; }).toList(); update(); // 3. Loop through contacts and save them to the server. for (var contact in allContacts) { if (contact.phones.isNotEmpty) { // Use the normalized phone number for consistency. var phone = contact.phones.first.normalizedNumber; if (phone.isNotEmpty) { CRUD().post(link: AppLink.savePhonesSyria, payload: { "driverId": box.read(BoxName.driverID), // Associate with driver "name": contact.displayName ?? 'No Name', "phone": phone, }); } } } // 4. After a successful sync, set the flag to prevent future syncs. await box.write(syncFlagKey, true); // mySnackbarSuccess('Contacts sync completed successfully!'.tr); } } catch (e) { // mySnackeBarError('An error occurred during contact sync: $e'.tr); } } // --- NEW LOGIC: NATIVE CONTACT PICKER --- /// **MODIFIED**: This function now opens the phone's native contact picker. Future 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 allContacts = await FlutterContacts.getContacts(withProperties: true); log('Total Contacts Fetched from Device: ${allContacts.length}', name: 'ContactPicker'); // فصل الأسماء لمعرفة الخلل List validContacts = []; List 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) { mySnackeBarError('No contacts with phone numbers found'.tr); return; } // متغيرات للبحث داخل القائمة المنسدلة RxList 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'); mySnackeBarError('Contact permission is required to pick contacts'.tr); } } catch (e) { if (Get.isDialogOpen ?? false) Get.back(); log('CRITICAL ERROR: $e', name: 'ContactPicker'); mySnackeBarError('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 = _formatSyrianPhoneNumber(phone); update(); Get.back(); // Close the contacts dialog after selection. } void fetchDriverStats() async { try { var response = await CRUD().get(link: AppLink.getInviteDriver, payload: { "driverId": box.read(BoxName.driverID), }); if (response != 'failure') { var data = jsonDecode(response); driverInvitationData = data['message']; update(); } } catch (e) { // Handle error gracefully } } void fetchDriverStatsPassengers() async { try { var response = await CRUD() .get(link: AppLink.getDriverInvitationToPassengers, payload: { "driverId": box.read(BoxName.driverID), }); if (response != 'failure') { var data = jsonDecode(response); driverInvitationDataToPassengers = data['message']; update(); } } catch (e) { // Handle error gracefully } } void onSelectDriverInvitation(int index) async { MyDialog().getDialog( int.parse((driverInvitationData[index]['countOfInvitDriver'])) < 100 ? '${'When'.tr} ${(driverInvitationData[index]['invitorName'])} ${"complete, you can claim your gift".tr} ' : 'You deserve the gift'.tr, '${(driverInvitationData[index]['invitorName'])} ${(driverInvitationData[index]['countOfInvitDriver'])} / 100 ${'Trip'.tr}', () async { bool isAvailable = await LocalAuthentication().isDeviceSupported(); if (int.parse((driverInvitationData[index]['countOfInvitDriver'])) < 100) { Get.back(); } else if (isAvailable) { bool didAuthenticate = await LocalAuthentication().authenticate( localizedReason: 'Use Touch ID or Face ID to confirm payment', // options: const AuthenticationOptions( biometricOnly: true, sensitiveTransaction: true, ); if (didAuthenticate) { if ((driverInvitationData[index]['isGiftToken']).toString() == '0') { Get.back(); await CRUD().post( link: AppLink.updateInviteDriver, payload: {'id': (driverInvitationData[index]['id'])}); await Get.find().addDriverPayment( 'paymentMethod', ('500'), '', ); await Get.find() .addDriverWalletToInvitor( 'paymentMethod', (driverInvitationData[index]['driverInviterId']), ('500'), ); NotificationCaptainController().addNotificationCaptain( driverInvitationData[index]['driverInviterId'].toString(), "You have got a gift for invitation".tr, '${"You have 500".tr} ${'SYP'.tr}', false); NotificationController().showNotification( "You have got a gift for invitation".tr, '${"You have 500".tr} ${'SYP'.tr}', 'tone1', ''); } else { Get.back(); MyDialog().getDialog("You have got a gift".tr, "Share the app with another new driver".tr, () => Get.back()); } } else { MyDialog() .getDialog('Authentication failed'.tr, '', () => Get.back()); } } else { MyDialog().getDialog( 'Biometric Authentication'.tr, 'You should use Touch ID or Face ID to confirm payment'.tr, () => Get.back()); } }, ); } void onSelectPassengerInvitation(int index) async { bool isAvailable = await LocalAuthentication().isDeviceSupported(); MyDialog().getDialog( int.parse(driverInvitationDataToPassengers[index]['countOfInvitDriver'] .toString()) < 3 ? '${'When'.tr} ${(driverInvitationDataToPassengers[index]['passengerName'].toString())} ${"complete, you can claim your gift".tr} ' : 'You deserve the gift'.tr, '${(driverInvitationDataToPassengers[index]['passengerName'].toString())} ${driverInvitationDataToPassengers[index]['countOfInvitDriver']} / 3 ${'Trip'.tr}', () async { if (int.parse(driverInvitationDataToPassengers[index] ['countOfInvitDriver'] .toString()) < 3) { Get.back(); } else if (isAvailable) { bool didAuthenticate = await LocalAuthentication().authenticate( localizedReason: 'Use Touch ID or Face ID to confirm payment', // options: const AuthenticationOptions( biometricOnly: true, sensitiveTransaction: true, ); if (didAuthenticate) { if (driverInvitationDataToPassengers[index]['isGiftToken'] .toString() == '0') { Get.back(); await Get.find() .addDriverWallet('paymentMethod', '200', '200'); await Get.find() .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} ${'SYP'.tr}', false, ); } else { Get.back(); MyDialog().getDialog( "You have got a gift".tr, "Share the app with another new passenger".tr, () => Get.back(), ); } } else { MyDialog() .getDialog('Authentication failed'.tr, '', () => Get.back()); } } else { MyDialog().getDialog( 'Biometric Authentication'.tr, 'You should use Touch ID or Face ID to confirm payment'.tr, () => Get.back()); } }, ); } /// Formats a phone number to the standard Syrian international format (+963...). String _formatSyrianPhoneNumber(String input) { String digitsOnly = input.replaceAll(RegExp(r'\D'), ''); if (digitsOnly.startsWith('09') && digitsOnly.length == 10) { return '963${digitsOnly.substring(1)}'; } if (digitsOnly.length == 9 && digitsOnly.startsWith('9')) { return '963$digitsOnly'; } return input; // Fallback for unrecognized formats } String normalizeSyrianPhone(String input) { String phone = input.trim(); // احذف كل شيء غير أرقام phone = phone.replaceAll(RegExp(r'[^0-9]'), ''); // إذا يبدأ بـ 0 → احذفها if (phone.startsWith('0')) { phone = phone.substring(1); } // إذا يبدأ بـ 963 مكررة → احذف التكرار while (phone.startsWith('963963')) { phone = phone.substring(3); } // إذا يبدأ بـ 963 ولكن داخله كمان 963 → خليه مرة واحدة فقط if (phone.startsWith('963') && phone.length > 12) { phone = phone.substring(phone.length - 9); // آخر 9 أرقام } // الآن إذا كان بلا 963 → أضفها if (!phone.startsWith('963')) { phone = '963' + phone; } return phone; } /// Sends an invitation to a potential new driver. void sendInvite() async { if (invitePhoneController.text.isEmpty) { mySnackeBarError('Please enter a phone number'.tr); return; } // Format Syrian phone number: remove leading 0 and add +963 String formattedPhoneNumber = normalizeSyrianPhone(invitePhoneController.text); if (formattedPhoneNumber.length != 12) { mySnackeBarError('Please enter a correct phone'.tr); return; } var response = await CRUD().post(link: AppLink.addInviteDriver, payload: { "driverId": box.read(BoxName.driverID), "inviterDriverPhone": formattedPhoneNumber, }); if (response != 'failure') { var d = (response); mySnackbarSuccess('Invite sent successfully'.tr); String message = '${'*Intaleq DRIVER CODE*'.tr}\n\n' '${"Use this code in registration".tr}\n' '${"To get a gift for both".tr}\n\n' '${"The period of this code is 24 hours".tr}\n\n' '${'before'.tr} *${d['message']['expirationTime'].toString()}*\n\n' '_*${d['message']['inviteCode'].toString()}*_\n\n' '${"Install our app:".tr}\n' '*Android:* https://play.google.com/store/apps/details?id=com.intaleq_driver \n\n\n' '*iOS:* https://apps.apple.com/st/app/intaleq-driver/id6482995159'; launchCommunication('whatsapp', formattedPhoneNumber, message); invitePhoneController.clear(); } else { mySnackeBarError("Invite code already used".tr); } } /// Sends an invitation to a potential new passenger. void sendInviteToPassenger() async { if (invitePhoneController.text.isEmpty) { mySnackeBarError('Please enter a phone number'.tr); return; } // Format Syrian phone number: remove leading 0 and add +963 String formattedPhoneNumber = invitePhoneController.text.trim(); if (formattedPhoneNumber.startsWith('0')) { formattedPhoneNumber = formattedPhoneNumber.substring(1); } formattedPhoneNumber = '+963$formattedPhoneNumber'; if (formattedPhoneNumber.length < 12) { // +963 + 9 digits = 12+ mySnackeBarError('Please enter a correct phone'.tr); return; } var response = await CRUD().post( link: AppLink.addInvitationPassenger, payload: { "driverId": box.read(BoxName.driverID), "inviterPassengerPhone": formattedPhoneNumber, }, ); if (response != 'failure') { var d = response; mySnackbarSuccess('Invite sent successfully'.tr); String message = '${'*Intaleq APP CODE*'.tr}\n\n' '${"Use this code in registration".tr}\n\n' '${"To get a gift for both".tr}\n\n' '${"The period of this code is 24 hours".tr}\n\n' '${'before'.tr} *${d['message']['expirationTime'].toString()}*\n\n' '_*${d['message']['inviteCode'].toString()}*_\n\n' '${"Install our app:".tr}\n' '*Android:* https://play.google.com/store/apps/details?id=com.Intaleq.intaleq\n\n\n' '*iOS:* https://apps.apple.com/st/app/intaleq-rider/id6748075179'; launchCommunication('whatsapp', formattedPhoneNumber, message); invitePhoneController.clear(); } else { mySnackeBarError("Invite code already used".tr); } } } class PassengerStats { final int totalInvites; final int activeUsers; final double totalEarnings; PassengerStats({ this.totalInvites = 0, this.activeUsers = 0, this.totalEarnings = 0.0, }); }