420 lines
16 KiB
Dart
Executable File
420 lines
16 KiB
Dart
Executable File
import 'dart:convert';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_contacts/contact.dart';
|
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:local_auth/local_auth.dart';
|
|
import 'package:sefer_driver/constant/box_name.dart';
|
|
import 'package:sefer_driver/constant/links.dart';
|
|
import 'package:sefer_driver/controller/functions/crud.dart';
|
|
import 'package:sefer_driver/controller/home/payment/captain_wallet_controller.dart';
|
|
import 'package:sefer_driver/main.dart';
|
|
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
|
import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
|
import 'package:share_plus/share_plus.dart';
|
|
|
|
import '../../firebase/local_notification.dart';
|
|
import '../../functions/launch.dart';
|
|
import '../../notification/notification_captain_controller.dart';
|
|
|
|
class InviteController extends GetxController {
|
|
final TextEditingController invitePhoneController = TextEditingController();
|
|
List driverInvitationData = [];
|
|
List driverInvitationDataToPassengers = [];
|
|
String? couponCode;
|
|
String? driverCouponCode;
|
|
|
|
// **FIX**: Added the missing 'contacts' and 'contactMaps' definitions.
|
|
List<Contact> contacts = [];
|
|
RxList<Map<String, dynamic>> contactMaps = <Map<String, dynamic>>[].obs;
|
|
|
|
int selectedTab = 0;
|
|
PassengerStats passengerStats = PassengerStats();
|
|
void updateSelectedTab(int index) {
|
|
selectedTab = index;
|
|
update();
|
|
}
|
|
|
|
Future<void> shareDriverCode() async {
|
|
if (driverCouponCode != null) {
|
|
final String shareText =
|
|
'''Join Intaleq as a driver using my referral code!
|
|
Use code: $driverCouponCode
|
|
Download the Intaleq Driver app now and earn rewards!
|
|
''';
|
|
await Share.share(shareText);
|
|
}
|
|
}
|
|
|
|
Future<void> sharePassengerCode() async {
|
|
if (couponCode != null) {
|
|
final String shareText = '''Get a discount on your first Intaleq ride!
|
|
Use my referral code: $couponCode
|
|
Download the Intaleq app now and enjoy your ride!
|
|
''';
|
|
await Share.share(shareText);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
// **MODIFIED**: Sync contacts automatically on controller initialization.
|
|
syncContactsToServerOnce();
|
|
// fetchDriverStats();
|
|
}
|
|
|
|
// --- NEW LOGIC: ONE-TIME CONTACTS SYNC ---
|
|
|
|
/// **NEW**: Syncs all phone contacts to the server, but only runs once per user.
|
|
Future<void> syncContactsToServerOnce() async {
|
|
final String syncFlagKey = 'contactsSynced_${box.read(BoxName.driverID)}';
|
|
|
|
// 1. Check if contacts have already been synced for this user.
|
|
if (box.read(syncFlagKey) == true) {
|
|
print("Contacts have already been synced for this user.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 2. Request permission and fetch all contacts.
|
|
if (await FlutterContacts.requestPermission(readonly: true)) {
|
|
// mySnackbarSuccess('Starting contacts sync in background...'.tr);
|
|
final List<Contact> allContacts =
|
|
await FlutterContacts.getContacts(withProperties: true);
|
|
// **FIX**: Assign fetched contacts to the class variable.
|
|
contacts = allContacts;
|
|
contactMaps.value = contacts.map((contact) {
|
|
return {
|
|
'name': contact.displayName,
|
|
'phones':
|
|
contact.phones.map((phone) => phone.normalizedNumber).toList(),
|
|
'emails': contact.emails.map((email) => email.address).toList(),
|
|
};
|
|
}).toList();
|
|
update();
|
|
|
|
// 3. Loop through contacts and save them to the server.
|
|
for (var contact in allContacts) {
|
|
if (contact.phones.isNotEmpty) {
|
|
// Use the normalized phone number for consistency.
|
|
var phone = contact.phones.first.normalizedNumber;
|
|
if (phone.isNotEmpty) {
|
|
await CRUD().post(link: AppLink.savePhonesSyria, payload: {
|
|
"driverId": box.read(BoxName.driverID), // Associate with driver
|
|
"name": contact.displayName ?? 'No Name',
|
|
"phone": phone,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. After a successful sync, set the flag to prevent future syncs.
|
|
await box.write(syncFlagKey, true);
|
|
mySnackbarSuccess('Contacts sync completed successfully!'.tr);
|
|
}
|
|
} catch (e) {
|
|
mySnackeBarError('An error occurred during contact sync: $e'.tr);
|
|
}
|
|
}
|
|
|
|
// --- NEW LOGIC: NATIVE CONTACT PICKER ---
|
|
|
|
/// **MODIFIED**: This function now opens the phone's native contact picker.
|
|
Future<void> pickContactFromNativeApp() async {
|
|
try {
|
|
if (await FlutterContacts.requestPermission(readonly: true)) {
|
|
// 1. Open the native contacts app to select a single contact.
|
|
final Contact? contact = await FlutterContacts.openExternalPick();
|
|
|
|
// 2. If a contact is selected and has a phone number...
|
|
if (contact != null && contact.phones.isNotEmpty) {
|
|
String selectedPhone = contact.phones.first.number;
|
|
|
|
// 3. Format the number and update the text field.
|
|
invitePhoneController.text = _formatSyrianPhoneNumber(selectedPhone);
|
|
update();
|
|
}
|
|
} else {
|
|
mySnackeBarError('Contact permission is required to pick contacts'.tr);
|
|
}
|
|
} catch (e) {
|
|
mySnackeBarError('An error occurred while picking contacts: $e'.tr);
|
|
}
|
|
}
|
|
|
|
/// **FIX**: Added the missing 'selectPhone' method.
|
|
void selectPhone(String phone) {
|
|
// Format the selected phone number and update the text field.
|
|
invitePhoneController.text = _formatSyrianPhoneNumber(phone);
|
|
update();
|
|
Get.back(); // Close the contacts dialog after selection.
|
|
}
|
|
|
|
void fetchDriverStats() async {
|
|
try {
|
|
var response = await CRUD().get(link: AppLink.getInviteDriver, payload: {
|
|
"driverId": box.read(BoxName.driverID),
|
|
});
|
|
if (response != 'failure') {
|
|
var data = jsonDecode(response);
|
|
driverInvitationData = data['message'];
|
|
update();
|
|
}
|
|
} catch (e) {
|
|
// Handle error gracefully
|
|
}
|
|
}
|
|
|
|
void fetchDriverStatsPassengers() async {
|
|
try {
|
|
var response = await CRUD()
|
|
.get(link: AppLink.getDriverInvitationToPassengers, payload: {
|
|
"driverId": box.read(BoxName.driverID),
|
|
});
|
|
if (response != 'failure') {
|
|
var data = jsonDecode(response);
|
|
driverInvitationDataToPassengers = data['message'];
|
|
update();
|
|
}
|
|
} catch (e) {
|
|
// Handle error gracefully
|
|
}
|
|
}
|
|
|
|
void onSelectDriverInvitation(int index) async {
|
|
MyDialog().getDialog(
|
|
int.parse((driverInvitationData[index]['countOfInvitDriver'])) < 100
|
|
? '${'When'.tr} ${(driverInvitationData[index]['invitorName'])} ${"complete, you can claim your gift".tr} '
|
|
: 'You deserve the gift'.tr,
|
|
'${(driverInvitationData[index]['invitorName'])} ${(driverInvitationData[index]['countOfInvitDriver'])} / 100 ${'Trip'.tr}',
|
|
() async {
|
|
bool isAvailable = await LocalAuthentication().isDeviceSupported();
|
|
if (int.parse((driverInvitationData[index]['countOfInvitDriver'])) <
|
|
100) {
|
|
Get.back();
|
|
} else if (isAvailable) {
|
|
bool didAuthenticate = await LocalAuthentication().authenticate(
|
|
localizedReason: 'Use Touch ID or Face ID to confirm payment',
|
|
options: const AuthenticationOptions(
|
|
biometricOnly: true,
|
|
sensitiveTransaction: true,
|
|
));
|
|
if (didAuthenticate) {
|
|
if ((driverInvitationData[index]['isGiftToken']).toString() ==
|
|
'0') {
|
|
Get.back();
|
|
await CRUD().post(
|
|
link: AppLink.updateInviteDriver,
|
|
payload: {'id': (driverInvitationData[index]['id'])});
|
|
await Get.find<CaptainWalletController>().addDriverPayment(
|
|
'paymentMethod',
|
|
('50000'),
|
|
'',
|
|
);
|
|
await Get.find<CaptainWalletController>()
|
|
.addDriverWalletToInvitor(
|
|
'paymentMethod',
|
|
(driverInvitationData[index]['driverInviterId']),
|
|
('50000'),
|
|
);
|
|
NotificationCaptainController().addNotificationCaptain(
|
|
driverInvitationData[index]['driverInviterId'].toString(),
|
|
"You have got a gift for invitation".tr,
|
|
'${"You have 50000".tr} ${'SYP'.tr}',
|
|
false);
|
|
NotificationController().showNotification(
|
|
"You have got a gift for invitation".tr,
|
|
'${"You have 50000".tr} ${'SYP'.tr}',
|
|
'tone1',
|
|
'');
|
|
} else {
|
|
Get.back();
|
|
MyDialog().getDialog("You have got a gift".tr,
|
|
"Share the app with another new driver".tr, () => Get.back());
|
|
}
|
|
} else {
|
|
MyDialog()
|
|
.getDialog('Authentication failed'.tr, '', () => Get.back());
|
|
}
|
|
} else {
|
|
MyDialog().getDialog(
|
|
'Biometric Authentication'.tr,
|
|
'You should use Touch ID or Face ID to confirm payment'.tr,
|
|
() => Get.back());
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
void onSelectPassengerInvitation(int index) async {
|
|
bool isAvailable = await LocalAuthentication().isDeviceSupported();
|
|
MyDialog().getDialog(
|
|
int.parse(driverInvitationDataToPassengers[index]['countOfInvitDriver']
|
|
.toString()) <
|
|
3
|
|
? '${'When'.tr} ${(driverInvitationDataToPassengers[index]['passengerName'].toString())} ${"complete, you can claim your gift".tr} '
|
|
: 'You deserve the gift'.tr,
|
|
'${(driverInvitationDataToPassengers[index]['passengerName'].toString())} ${driverInvitationDataToPassengers[index]['countOfInvitDriver']} / 3 ${'Trip'.tr}',
|
|
() async {
|
|
if (int.parse(driverInvitationDataToPassengers[index]
|
|
['countOfInvitDriver']
|
|
.toString()) <
|
|
3) {
|
|
Get.back();
|
|
} else if (isAvailable) {
|
|
bool didAuthenticate = await LocalAuthentication().authenticate(
|
|
localizedReason: 'Use Touch ID or Face ID to confirm payment',
|
|
options: const AuthenticationOptions(
|
|
biometricOnly: true,
|
|
sensitiveTransaction: true,
|
|
));
|
|
if (didAuthenticate) {
|
|
if (driverInvitationDataToPassengers[index]['isGiftToken']
|
|
.toString() ==
|
|
'0') {
|
|
Get.back();
|
|
await Get.find<CaptainWalletController>()
|
|
.addDriverWallet('paymentMethod', '20000', '20000');
|
|
await Get.find<CaptainWalletController>()
|
|
.addDriverWalletToInvitor('paymentMethod',
|
|
driverInvitationData[index]['driverInviterId'], '20000');
|
|
await CRUD().post(
|
|
link: AppLink.updatePassengerGift,
|
|
payload: {'id': driverInvitationDataToPassengers[index]['id']},
|
|
);
|
|
NotificationCaptainController().addNotificationCaptain(
|
|
driverInvitationDataToPassengers[index]['passengerInviterId']
|
|
.toString(),
|
|
"You have got a gift for invitation".tr,
|
|
'${"You have 20000".tr} ${'SYP'.tr}',
|
|
false,
|
|
);
|
|
} else {
|
|
Get.back();
|
|
MyDialog().getDialog(
|
|
"You have got a gift".tr,
|
|
"Share the app with another new passenger".tr,
|
|
() => Get.back(),
|
|
);
|
|
}
|
|
} else {
|
|
MyDialog()
|
|
.getDialog('Authentication failed'.tr, '', () => Get.back());
|
|
}
|
|
} else {
|
|
MyDialog().getDialog(
|
|
'Biometric Authentication'.tr,
|
|
'You should use Touch ID or Face ID to confirm payment'.tr,
|
|
() => Get.back());
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Formats a phone number to the standard Syrian international format (+963...).
|
|
String _formatSyrianPhoneNumber(String input) {
|
|
String digitsOnly = input.replaceAll(RegExp(r'\D'), '');
|
|
if (digitsOnly.startsWith('963')) {
|
|
return '+$digitsOnly';
|
|
}
|
|
if (digitsOnly.startsWith('09') && digitsOnly.length == 10) {
|
|
return '+963${digitsOnly.substring(1)}';
|
|
}
|
|
if (digitsOnly.length == 9 && digitsOnly.startsWith('9')) {
|
|
return '+963$digitsOnly';
|
|
}
|
|
return input; // Fallback for unrecognized formats
|
|
}
|
|
|
|
/// Sends an invitation to a potential new driver.
|
|
void sendInvite() async {
|
|
if (invitePhoneController.text.isEmpty) {
|
|
mySnackeBarError('Please enter a phone number'.tr);
|
|
return;
|
|
}
|
|
String formattedPhoneNumber =
|
|
_formatSyrianPhoneNumber(invitePhoneController.text);
|
|
if (formattedPhoneNumber.length < 13) {
|
|
mySnackeBarError('Please enter a correct phone'.tr);
|
|
return;
|
|
}
|
|
|
|
var response = await CRUD().post(link: AppLink.addInviteDriver, payload: {
|
|
"driverId": box.read(BoxName.driverID),
|
|
"inviterDriverPhone": formattedPhoneNumber,
|
|
});
|
|
|
|
if (response != 'failure') {
|
|
var d = (response);
|
|
mySnackbarSuccess('Invite sent successfully'.tr);
|
|
String message = '${'*Intaleq DRIVER CODE*'.tr}\n\n'
|
|
'${"Use this code in registration".tr}\n'
|
|
'${"To get a gift for both".tr}\n\n'
|
|
'${"The period of this code is 1 hour".tr}\n\n'
|
|
'${'before'.tr} *${d['message']['expirationTime'].toString()}*\n\n'
|
|
'_*${d['message']['inviteCode'].toString()}*_\n\n'
|
|
'${"Install our app:".tr}\n'
|
|
'*Android:* https://play.google.com/store/apps/details?id=com.intaleq_driver\n\n\n'
|
|
'*iOS:* https://apps.apple.com/st/app/intaleq-driver/id6482995159';
|
|
|
|
launchCommunication('whatsapp', formattedPhoneNumber, message);
|
|
invitePhoneController.clear();
|
|
} else {
|
|
mySnackeBarError("Invite code already used".tr);
|
|
}
|
|
}
|
|
|
|
/// Sends an invitation to a potential new passenger.
|
|
void sendInviteToPassenger() async {
|
|
if (invitePhoneController.text.isEmpty) {
|
|
mySnackeBarError('Please enter a phone number'.tr);
|
|
return;
|
|
}
|
|
String formattedPhoneNumber =
|
|
_formatSyrianPhoneNumber(invitePhoneController.text);
|
|
if (formattedPhoneNumber.length < 13) {
|
|
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 = jsonDecode(response);
|
|
mySnackbarSuccess('Invite sent successfully'.tr);
|
|
String message = '${'*Intaleq APP CODE*'.tr}\n\n'
|
|
'${"Use this code in registration".tr}\n\n'
|
|
'${"To get a gift for both".tr}\n\n'
|
|
'${"The period of this code is 1 hour".tr}\n\n'
|
|
'${'before'.tr} *${d['message']['expirationTime'].toString()}*\n\n'
|
|
'_*${d['message']['inviteCode'].toString()}*_\n\n'
|
|
'${"Install our app:".tr}\n'
|
|
'*Android:* https://play.google.com/store/apps/details?id=com.Intaleq.intaleq\n\n\n'
|
|
'*iOS:* https://apps.apple.com/st/app/intaleq-rider/id6748075179';
|
|
|
|
launchCommunication('whatsapp', formattedPhoneNumber, message);
|
|
invitePhoneController.clear();
|
|
} else {
|
|
mySnackeBarError("Invite code already used".tr);
|
|
}
|
|
}
|
|
}
|
|
|
|
class PassengerStats {
|
|
final int totalInvites;
|
|
final int activeUsers;
|
|
final double totalEarnings;
|
|
|
|
PassengerStats({
|
|
this.totalInvites = 0,
|
|
this.activeUsers = 0,
|
|
this.totalEarnings = 0.0,
|
|
});
|
|
}
|