first commit

This commit is contained in:
Hamza-Ayed
2026-06-09 08:40:31 +03:00
commit d8901e1a87
3161 changed files with 536187 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
// import 'package:home_widget/home_widget.dart';
// class TripzHomeWidgetProvider {
// static const String widgetName = 'TripzHomeWidget';
// // Initialize Home Widget
// static Future<void> initHomeWidget() async {
// await HomeWidget.registerInteractivityCallback(backgroundCallback);
// }
// // Background Callback for Widget Updates
// static Future<void> backgroundCallback(Uri? uri) async {
// if (uri?.host == 'updateWidget') {
// // Logic to update widget data
// await updateWidgetData();
// }
// }
// // Update Widget Data Method
// static Future<void> updateWidgetData() async {
// // Fetch current ride details
// final rideData = await _fetchCurrentRideDetails();
// // Update Widget with Ride Information
// await HomeWidget.saveWidgetData<String>(
// 'ride_destination', rideData.destination);
// await HomeWidget.saveWidgetData<String>(
// 'ride_estimated_time', rideData.estimatedTime);
// await HomeWidget.saveWidgetData<double>('ride_fare', rideData.fare);
// // Trigger Widget Update
// await HomeWidget.updateWidget(
// name: widgetName,
// iOSName: 'TripzWidgetProvider',
// androidName: 'com.mobileapp.store.ride.HomeWidgetProvider',
// );
// }
// // Mock method to fetch ride details (replace with actual implementation)
// static Future<RideData> _fetchCurrentRideDetails() async {
// // Implement actual data fetching logic
// return RideData(
// destination: 'Downtown Office', estimatedTime: '25 mins', fare: 15.50);
// }
// }
// // Ride Data Model
// class RideData {
// final String destination;
// final String estimatedTime;
// final double fare;
// RideData(
// {required this.destination,
// required this.estimatedTime,
// required this.fare});
// }

View File

@@ -0,0 +1,37 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:get/get.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
class AuthController extends GetxController {
final FirebaseAuth _auth = FirebaseAuth.instance;
Future<User?> signInWithApple() async {
try {
final appleCredential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
);
final oAuthProvider = OAuthProvider('apple.com');
final credential = oAuthProvider.credential(
idToken: appleCredential.identityToken,
accessToken: appleCredential.authorizationCode,
);
UserCredential userCredential =
await _auth.signInWithCredential(credential);
return userCredential.user;
} catch (error) {
return null;
}
}
Future<void> signOut() async {
try {
await _auth.signOut();
} catch (error) {}
}
}

View File

@@ -0,0 +1,41 @@
import 'package:siro_rider/print.dart';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
Future<bool> verifyCertificateManually(
String host, int port, String expectedPin) async {
try {
final socket = await SecureSocket.connect(host, port,
timeout: const Duration(seconds: 5));
final certificate = socket.peerCertificate;
if (certificate == null) {
Log.print("❌ لا يوجد شهادة.");
return false;
}
final der = certificate.der;
final actualPin = base64.encode(sha256.convert(der).bytes);
Log.print("📛 HOST: $host");
Log.print("📜 Subject: ${certificate.subject}");
Log.print("📜 Issuer: ${certificate.issuer}");
Log.print("📅 Valid From: ${certificate.startValidity}");
Log.print("📅 Valid To: ${certificate.endValidity}");
Log.print(
"🔐 Server Pin: $actualPin${actualPin == expectedPin ? '✅ MATCH' : '❌ MISMATCH'}");
socket.destroy();
return actualPin == expectedPin;
} catch (e) {
Log.print("❌ خطأ أثناء الاتصال أو الفحص: $e");
return false;
}
}
/// تحويل المفتاح العام إلى بصمة SHA-256
List<int> sha256Convert(Uint8List der) {
return sha256.convert(der).bytes;
}

View File

@@ -0,0 +1,149 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../constant/box_name.dart';
import '../../constant/links.dart';
import '../../main.dart';
import '../../print.dart';
import '../functions/crud.dart';
import '../../onbording_page.dart';
import 'login_controller.dart';
class GoogleSignInHelper {
// ✅ GoogleSignIn singleton
static final GoogleSignIn _signIn = GoogleSignIn.instance;
static GoogleSignInAccount? _lastUser;
/// 👇 استدعها في main() مرة واحدة
static Future<void> init() async {
await _signIn.initialize(
serverClientId:
'594687661098-2u640akrb3k7sak5t0nqki6f4v6hq1bq.apps.googleusercontent.com',
);
// Events listener
_signIn.authenticationEvents.listen((event) async {
if (event is GoogleSignInAuthenticationEventSignIn) {
_lastUser = event.user;
await _handleSignUp(event.user);
} else if (event is GoogleSignInAuthenticationEventSignOut) {
_lastUser = null;
}
});
// silent login if possible
try {
await _signIn.attemptLightweightAuthentication();
} catch (e) {
Log.print("Error: $e");
}
}
/// ✅ تسجيل دخول عادي
static Future<GoogleSignInAccount?> signIn() async {
try {
final user =
await _signIn.authenticate(scopeHint: const ['email', 'profile']);
_lastUser = user;
await _handleSignUp(user);
// اطبع القيم (للتأكد)
Log.print("Google ID: ${user.id}");
Log.print("Email: ${user.email}");
Log.print("Name: ${user.displayName}");
Log.print("Photo: ${user.photoUrl}");
return user;
} on PlatformException catch (e) {
if (e.code == 'sign_in_required') {
await showGooglePlayServicesError();
}
return null;
} catch (e) {
await addError("Google Sign-In error: $e", "signIn()");
return null;
}
}
/// ✅ تسجيل دخول مخصص لشاشة Login (مع استدعاء الكنترولر)
static Future<GoogleSignInAccount?> signInFromLogin() async {
await init();
final user = await signIn();
if (user != null) {
await Get.put(LoginController()).loginUsingCredentials(
box.read(BoxName.passengerID).toString(),
box.read(BoxName.email).toString(),
);
}
return user;
}
/// ✅ طلب سكوبات إضافية (بديل withScopes القديم)
static Future<void> requestExtraScopes(List<String> scopes) async {
final user = _lastUser;
if (user == null) return;
await user.authorizationClient.authorizeScopes(scopes);
}
/// ✅ تسجيل خروج
static Future<void> signOut() async {
await _signIn.signOut();
await _handleSignOut();
}
static GoogleSignInAccount? getCurrentUser() => _lastUser;
// ================= Helpers ==================
static Future<void> _handleSignUp(GoogleSignInAccount user) async {
box.write(BoxName.passengerID, user.id);
box.write(BoxName.email, user.email);
box.write(BoxName.name, user.displayName ?? '');
box.write(BoxName.passengerPhotoUrl, user.photoUrl ?? '');
}
static Future<void> _handleSignOut() async {
box.erase();
Get.offAll(OnBoardingPage());
}
static Future<void> addError(String error, String where) async {
await CRUD().post(link: AppLink.addError, payload: {
'error': error,
'userId': box.read(BoxName.driverID) ?? box.read(BoxName.passengerID),
'userType': box.read(BoxName.driverID) != null ? 'Driver' : 'Passenger',
'phone': box.read(BoxName.phone) ?? box.read(BoxName.phoneDriver),
'device': where,
});
}
static Future<void> showGooglePlayServicesError() async {
const playStoreUrl =
'https://play.google.com/store/apps/details?id=com.google.android.gms&hl=en_US';
final uri = Uri.parse(playStoreUrl);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
} else {
showDialog(
context: Get.context!,
builder: (context) => AlertDialog(
title: Text('Error'.tr),
content: Text(
'Could not open the Google Play Store. Please update Google Play Services manually.'
.tr,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Close'.tr),
),
],
),
);
}
}
}

View File

@@ -0,0 +1,526 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:siro_rider/constant/api_key.dart';
import 'package:siro_rider/controller/firebase/firbase_messge.dart';
import 'package:siro_rider/views/auth/otp_page.dart';
import 'package:siro_rider/views/widgets/error_snakbar.dart';
import 'package:http/http.dart' as http;
import 'package:siro_rider/constant/info.dart';
import 'package:siro_rider/controller/functions/add_error.dart';
import 'package:siro_rider/views/auth/login_page.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/constant/links.dart';
import 'package:siro_rider/controller/functions/crud.dart';
import 'package:siro_rider/main.dart';
import 'package:siro_rider/views/home/map_page_passenger.dart';
import 'package:location/location.dart';
import '../../env/env.dart';
import '../../print.dart';
import '../../views/auth/otp_token_page.dart';
import '../functions/encrypt_decrypt.dart';
import '../functions/package_info.dart';
class LoginController extends GetxController {
final formKey = GlobalKey<FormState>();
final formKeyAdmin = GlobalKey<FormState>();
TextEditingController emailController = TextEditingController();
TextEditingController phoneController = TextEditingController();
TextEditingController passwordController = TextEditingController();
TextEditingController adminPasswordController = TextEditingController();
TextEditingController adminNameController = TextEditingController();
bool isAgreeTerms = false;
bool isloading = false;
late int isTest = 1;
void changeAgreeTerm() {
isAgreeTerms = !isAgreeTerms;
update();
}
var dev = '';
@override
void onInit() async {
// Log.print('box.read(BoxName.isTest): ${box.read(BoxName.isTest)}');
box.write(BoxName.countryCode, 'Syria');
FirebaseMessagesController().getToken();
super.onInit();
}
void saveAgreementTerms() {
box.write(BoxName.agreeTerms, 'agreed');
update();
}
void saveCountryCode(String countryCode) {
box.write(BoxName.countryCode, countryCode);
update();
}
// ═══════════════════════════════════════════════════════════════
// LoginController — دوال إدارة الـ JWT
// ───────────────────────────────────────────────────────────────
// لا تغيير على اسم الكلاس أو أسماء الدوال
// ═══════════════════════════════════════════════════════════════
// داخل class LoginController
// ─────────────────────────────────────────────────────────────
// getJWT: الحصول على توكن للراكب
// ─────────────────────────────────────────────────────────────
// المنطق:
// • firstTimeLoadKey != false ← أول مرة يفتح التطبيق → loginFirstTime
// • firstTimeLoadKey == false ← مستخدم موجود → loginJwtRider
// ─────────────────────────────────────────────────────────────
Future<void> getJWT() async {
// إذا كان التوكن الحالي لا يزال صالحاً، لا داعي لطلب واحد جديد
if (isTokenValid()) {
Log.print("JWT is still valid. Skipping request.");
return;
}
try {
dev = Platform.isAndroid ? 'android' : 'ios';
// تأكد إن البصمة محدّثة قبل أي طلب
await DeviceHelper.getDeviceFingerprint();
final String fp = box.read(BoxName.deviceFpEncrypted) ?? '';
if (box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
// ── أول تسجيل ─────────────────────────────────────────
// نرسل البصمة المشفرة مع باقي البيانات
// السيرفر سيعمل hash لها ويخزنها في JWT payload
var payload = {
'id': box.read(BoxName.passengerID) ?? AK.newId,
'password': AK.passnpassenger,
'aud': '${AK.allowed}$dev',
'fingerPrint': fp,
};
var response = await http.post(
Uri.parse(AppLink.loginFirstTime),
body: payload,
);
Log.print('AppLink.loginFirstTime: ${AppLink.loginFirstTime}');
Log.print('payload: $payload');
Log.print('response: $response');
if (response.statusCode == 200) {
final decoded = jsonDecode(response.body);
final String? jwt = decoded['data'] != null
? decoded['data']['jwt']
: (decoded['message'] != null
? decoded['message']['jwt']
: decoded['jwt']);
if (jwt != null) {
// نشفر الـ JWT بالتشفير الثلاثي قبل التخزين في GetStorage
box.write(BoxName.jwt, c(jwt));
}
await EncryptionHelper.initialize();
}
} else {
// ── مستخدم موجود: تجديد التوكن ────────────────────────
await EncryptionHelper.initialize();
var payload = {
'id': box.read(BoxName.passengerID),
'fingerPrint': fp,
'aud': '${AK.allowed}$dev',
};
var response = await http.post(
Uri.parse(AppLink.loginJwtRider),
body: payload,
);
Log.print('AppLink.loginJwtRider: ${AppLink.loginJwtRider}');
Log.print('payload: $payload');
Log.print('response: ${response.body}');
if (response.statusCode == 200) {
final decoded = jsonDecode(response.body);
final String? jwt = decoded['data'] != null
? decoded['data']['jwt']
: (decoded['message'] != null
? decoded['message']['jwt']
: decoded['jwt']);
if (jwt != null) {
box.write(BoxName.jwt, c(jwt));
}
}
}
} catch (e) {
Log.print('Error in getJWT: $e');
}
}
// ─────────────────────────────────────────────────────────────
// التحقق من صلاحية التوكن يدوياً (بدون مكاتب خارجية)
// ─────────────────────────────────────────────────────────────
bool isTokenValid() {
try {
final String? encryptedJwt = box.read(BoxName.jwt);
if (encryptedJwt == null || encryptedJwt.isEmpty) {
Log.print("isTokenValid: No token found in storage.");
return false;
}
// 1. فك تشفير التوكن المخزن
final String jwtFull = r(encryptedJwt).toString();
final String jwt = jwtFull.split(Env.addd)[0];
final parts = jwt.split('.');
if (parts.length != 3) {
Log.print("isTokenValid: Invalid JWT format (parts: ${parts.length})");
return false;
}
// 2. فك Base64 للجزء الأوسط (Payload)
String payloadPart = parts[1];
while (payloadPart.length % 4 != 0) {
payloadPart += '=';
}
final String decodedPayload = utf8.decode(base64Url.decode(payloadPart));
final Map<String, dynamic> payload = jsonDecode(decodedPayload);
if (!payload.containsKey('exp')) {
Log.print("isTokenValid: No 'exp' claim in token.");
return false;
}
final int exp = payload['exp'];
final int now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
Log.print("isTokenValid: Now=$now, Exp=$exp, Diff=${exp - now}s");
// اعتبار التوكن منتهي قبل دقيقة للأمان
final bool valid = exp > (now + 60);
Log.print("isTokenValid: Result=$valid");
return valid;
} catch (e) {
Log.print("isTokenValid Error: $e");
return false;
}
}
// ─────────────────────────────────────────────────────────────
// الفرق عن getJWT:
// • يستخدم endpoint مختلف (loginWallet)
// • يرجع hmac مع الـ jwt ويخزنه في GetStorage
// • الـ JWT لا يُشفَّر ثلاثياً (يُستخدم مباشرة في الـ header)
// ─────────────────────────────────────────────────────────────
Future<String?> getJwtWallet() async {
dev = Platform.isAndroid ? 'android' : 'ios';
final String fp = box.read(BoxName.deviceFpEncrypted) ?? '';
var payload = {
'id': box.read(BoxName.passengerID),
'password': AK.passnpassenger,
'aud': '${AK.allowedWallet}$dev',
'fingerPrint': fp,
};
var response = await http.post(
Uri.parse(AppLink.loginJwtWalletRider),
body: payload,
);
Log.print('AppLink.loginJwtWalletRider: ${AppLink.loginJwtWalletRider}');
Log.print('response wallet: ${response.body}');
if (response.statusCode == 200) {
final decoded = jsonDecode(response.body);
// ← الإصلاح: نقرأ من message أو data أو root
final inner = decoded['data'] ?? decoded['message'] ?? decoded;
final String? jwt = inner['jwt'];
final String? hmac = inner['hmac'];
Log.print('jwt extracted: $jwt');
Log.print('hmac extracted: $hmac');
if (hmac != null) {
box.write(BoxName.hmac, hmac);
}
return jwt;
}
return null;
}
Future<void> loginUsingCredentials(String passengerID, String email) async {
isloading = true;
update();
// await getJWT();
Log.print("LoginController.loginUsingCredentials: ");
try {
// 1) استعلام تسجيل الدخول
final res = await CRUD().get(
link: AppLink.loginFromGooglePassenger,
payload: {
'email': email,
'id': passengerID, // استخدم المعامل مباشرة
'platform': Platform.isAndroid ? 'android' : 'ios',
'appName': AppInformation.appName,
},
);
// 2) فك JSON مرة واحدة والتحقق من النجاح
final decoded = jsonDecode(res);
if (decoded is! Map || decoded.isEmpty) return;
if (decoded['status'] == 'failure' || decoded['status'] == 'Failure') {
Get.snackbar("User does not exist.".tr, '',
backgroundColor: Colors.red);
return;
}
final status = (decoded['status'] ?? '').toString();
final data = (decoded['data'] as List?)?.firstOrNull as Map? ?? {};
if (status != 'success' || data['verified'].toString() != '1') {
// غير مُفعل -> أذهب للتسجيل بالـ SMS
Get.offAll(() => PhoneNumberScreen());
return;
}
// 3) كتابة القيم (خفيفة)
box.write(BoxName.isVerified, '1');
box.write(BoxName.email, data['email']);
box.write(BoxName.phone, data['phone']);
box.write(BoxName.name, data['first_name']);
box.write(BoxName.isTest, '1');
box.write(BoxName.package, data['package']);
box.write(BoxName.promo, data['promo']);
box.write(BoxName.discount, data['discount']);
box.write(BoxName.validity, data['validity']);
box.write(BoxName.isInstall, data['isInstall'] ?? 'none');
box.write(BoxName.isGiftToken, data['isGiftToken'] ?? 'none');
if (data['inviteCode'] != null) {
box.write(BoxName.inviteCode, data['inviteCode'].toString());
}
// مهم: تأكد من passengerID في الـ box
box.write(BoxName.passengerID, passengerID);
// 4) فحص ما إذا كان التوكن موجوداً في رد الـ Login المدمج (V3 Optimization)
String? serverFCM;
String? serverFP;
var userData = decoded['data'] is List
? (decoded['data'] as List).firstOrNull
: decoded['data'];
if (userData is Map && userData['fcm_token'] != null) {
serverFCM = userData['fcm_token'].toString();
serverFP = userData['fcm_fingerprint']?.toString() ?? '';
Log.print(
"✅ FCM Token found in Login response. Skipping separate getTokens call.");
}
// إذا لم يكن موجوداً (توافقية قديمة)، نقوم بطلبه
String? tokenResp;
if (serverFCM == null) {
tokenResp = await CRUD().get(
link: AppLink.getTokens, payload: {'passengerID': passengerID});
}
final localFP = (await DeviceHelper.getDeviceFingerprint()).toString();
await storage.write(key: BoxName.fingerPrint, value: localFP);
await box.write(BoxName.firstTimeLoadKey, 'false');
// ── 5. المقارنة: FCM token + fingerprint ──────────────────────
if (email != '962798583052@intaleqapp.com') {
if (serverFCM == null &&
tokenResp != null &&
tokenResp != 'failure' &&
tokenResp != 'error') {
final tokenJson = jsonDecode(tokenResp);
final serverData = tokenJson['data'] ?? tokenJson['message'];
if (serverData is Map) {
serverFCM = serverData['token']?.toString() ?? '';
serverFP = serverData['fingerPrint']?.toString() ?? '';
}
}
if (serverFCM != null && serverFCM.isNotEmpty) {
final localFCM = (box.read(BoxName.tokenFCM) ?? '').toString();
// ── اختلاف أي منهما = جهاز مختلف أو تثبيت جديد ─────────
final fcmChanged = serverFCM != localFCM;
final fpChanged =
serverFP != null && serverFP.isNotEmpty && serverFP != localFP;
if (fcmChanged || fpChanged) {
mySnackbarInfo('Device Change Detected'.tr);
await Get.to(() => OtpVerificationPage(
phone: data['phone'].toString(),
deviceToken: localFP,
token: tokenResp ?? '',
ptoken: serverFCM ?? '', // نمرر FCM القديم للـ OTP controller
));
return;
}
}
}
// 6) منطق الدعوة (إن وُجد) ثم الانتقال
final invite = box.read(BoxName.inviteCode)?.toString() ?? 'none';
final isInstall = box.read(BoxName.isInstall)?.toString() ?? '0';
if (invite != 'none' && isInstall != '1') {
try {
await CRUD().post(link: AppLink.updatePassengersInvitation, payload: {
"inviteCode": invite,
"passengerID": passengerID,
});
await Get.defaultDialog(
title: 'Invitation Used'.tr,
middleText: "Your invite code was successfully applied!".tr,
textConfirm: "OK".tr,
confirmTextColor: Colors.white,
onConfirm: () async {
try {
await CRUD().post(link: AppLink.addPassengersPromo, payload: {
"promoCode":
'I-${(box.read(BoxName.name)).toString().split(' ').first}',
"amount": '25',
"passengerID": passengerID,
"description": 'promo first'
});
} catch (e) {
addError(
e.toString(), 'promo on invitation in login_controller');
} finally {
Get.offAll(() => const MapPagePassenger());
}
},
);
return;
} catch (_) {
// حتى لو فشل، كمل للصفحة الرئيسية
}
}
Get.offAll(() => const MapPagePassenger());
} catch (e) {
addError('$e', 'loginUsingCredentials');
Get.snackbar('Error', e.toString(), backgroundColor: Colors.redAccent);
} finally {
isloading = false;
update();
}
}
// Future<bool?> _confirmDeviceChangeDialog() {
// return Get.defaultDialog<bool>(
// barrierDismissible: false,
// title: 'Device Change Detected'.tr,
// middleText: 'Please verify your identity'.tr,
// textConfirm: 'Verify'.tr,
// confirmTextColor: Colors.white,
// onConfirm: () => Get.back(result: true),
// textCancel: 'Cancel'.tr,
// onCancel: () => Get.back(result: false),
// );
// }
void login() async {
isloading = true;
update();
var res =
await CRUD().get(link: AppLink.loginFromGooglePassenger, payload: {
'email': (emailController.text),
'id': passwordController.text,
"platform": Platform.isAndroid ? 'android' : 'ios',
"appName": AppInformation.appName,
});
isloading = false;
update();
if (res == 'Failure') {
//Failure
Get.offAll(() => LoginPage());
isloading = false;
update();
// Get.snackbar("User does not exist.".tr, '', backgroundColor: Colors.red);
} else {
var jsonDecoeded = jsonDecode(res);
if (jsonDecoeded.isNotEmpty) {
if (jsonDecoeded['status'] == 'success' &&
jsonDecoeded['data'][0]['verified'].toString() == '1') {
//
box.write(BoxName.isVerified, '1');
box.write(BoxName.email, jsonDecoeded['data'][0]['email']);
box.write(BoxName.name, jsonDecoeded['data'][0]['first_name']);
box.write(BoxName.phone, jsonDecoeded['data'][0]['phone']);
box.write(BoxName.passengerID, passwordController.text);
// var token = await CRUD().get(link: AppLink.getTokens, payload: {
// 'passengerID': box.read(BoxName.passengerID).toString()
// });
// await updateAppTester(AppInformation.appName);
Get.offAll(() => const MapPagePassenger());
} else {
// Get.offAll(() => SmsSignupEgypt());
// Get.snackbar(jsonDecoeded['status'], jsonDecoeded['data'],
// backgroundColor: Colors.redAccent);
isloading = false;
update();
}
} else {
isloading = false;
update();
}
}
}
void goToMapPage() {
if (box.read(BoxName.email) != null) {
Get.offAll(() => const MapPagePassenger());
}
}
final location = Location();
// late PermissionStatus permissionGranted = PermissionStatus.denied;
Future<void> getLocationPermission() async {
bool serviceEnabled;
PermissionStatus permissionGranted;
// Check if location services are enabled
serviceEnabled = await location.serviceEnabled();
if (!serviceEnabled) {
serviceEnabled = await location.requestService();
if (!serviceEnabled) {
// Location services are still not enabled, handle the error
return;
}
}
// Check if the app has permission to access location
permissionGranted = await location.hasPermission();
if (permissionGranted == PermissionStatus.denied) {
permissionGranted = await location.requestPermission();
if (permissionGranted != PermissionStatus.granted) {
// Location permission is still not granted, handle the error
permissionGranted = await location.requestPermission();
return;
}
}
if (permissionGranted.toString() == 'PermissionStatus.granted') {
box.write(BoxName.locationPermission, 'true');
}
update();
}
}

View File

@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/main.dart';
import 'package:siro_rider/views/auth/login_page.dart';
import '../../models/model/onboarding_model.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(() => LoginPage());
} 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();
}
}

View File

@@ -0,0 +1,210 @@
import 'package:siro_rider/constant/links.dart';
import 'package:siro_rider/views/home/map_page_passenger.dart';
import 'package:get/get.dart';
import '../../../constant/box_name.dart';
import '../../../main.dart';
import '../../../print.dart';
import '../../views/auth/otp_page.dart';
import '../../views/widgets/error_snakbar.dart';
import '../functions/crud.dart';
import '../functions/package_info.dart';
import 'login_controller.dart';
// --- Helper Class for Phone Authentication ---
class PhoneAuthHelper {
// Define your server URLs
static final String _baseUrl = '${AppLink.server}/auth/syria/';
static final String _sendOtpUrl = '${_baseUrl}sendWhatsOpt.php';
static final String _verifyOtpUrl = '${_baseUrl}verifyOtp.php';
static final String _registerUrl = '${_baseUrl}register_passenger.php';
static String formatSyrianPhone(String phone) {
// Remove spaces, symbols, +, -, ()
phone = phone.replaceAll(RegExp(r'[ \-\(\)\+]'), '').trim();
// Normalize 00963 → 963
if (phone.startsWith('00963')) {
phone = phone.replaceFirst('00963', '963');
}
// Normalize 0963 → 963
if (phone.startsWith('0963')) {
phone = phone.replaceFirst('0963', '963');
}
// NEW: Fix 96309xxxx → 9639xxxx
if (phone.startsWith('96309')) {
phone = '9639' + phone.substring(5); // remove the "0" after 963
}
// If starts with 9630 → correct to 9639
if (phone.startsWith('9630')) {
phone = '9639' + phone.substring(4);
}
// If already in correct format: 9639xxxxxxxx
if (phone.startsWith('9639') && phone.length == 12) {
return phone;
}
// If starts with 963 but missing the 9
if (phone.startsWith('963') && phone.length > 3) {
// Ensure it begins with 9639
if (!phone.startsWith('9639')) {
phone = '9639' + phone.substring(3);
}
return phone;
}
// If starts with 09xxxxxxxx → 9639xxxxxxxx
if (phone.startsWith('09')) {
return '963' + phone.substring(1);
}
// If 9xxxxxxxx (9 digits)
if (phone.startsWith('9') && phone.length == 9) {
return '963' + phone;
}
// If starts with incorrect 0xxxxxxx → assume Syrian and fix
if (phone.startsWith('0') && phone.length == 10) {
return '963' + phone.substring(1);
}
return phone;
}
/// Sends an OTP to the provided phone number.
static Future<bool> sendOtp(String phoneNumber) async {
try {
// إصلاح الرقم قبل الإرسال
final fixedPhone = formatSyrianPhone(phoneNumber);
final response = await CRUD().post(
link: _sendOtpUrl,
payload: {'receiver': fixedPhone}, // ← ← استخدام الرقم المُعدّل
);
if (response != 'failure') {
final data = response;
if (data['status'] == 'success') {
mySnackbarSuccess('An OTP has been sent to your number.'.tr);
return true;
} else {
mySnackeBarError(data['message'] ?? 'Failed to send OTP.');
return false;
}
} else {
mySnackeBarError('Server error. Please try again.'.tr);
return false;
}
} catch (e) {
return false;
}
}
/// Verifies the OTP and logs the user in.
static Future<void> verifyOtp(String phoneNumber, String otpCode) async {
try {
final fixedPhone = formatSyrianPhone(phoneNumber);
final response = await CRUD().post(
link: _verifyOtpUrl,
payload: {
'phone_number': fixedPhone,
'otp': otpCode,
},
);
if (response != 'failure') {
final data = (response);
Log.print('data: ${data}');
if (data['status'] == 'success') {
final isRegistered = data['message']['isRegistered'] ?? false;
Log.print('isRegistered: ${isRegistered}');
if (isRegistered) {
// ✅ المستخدم موجود مسبقاً -> تسجيل دخول مباشر
await _handleSuccessfulLogin(data['message']['passenger']);
} else {
// ✅ مستخدم جديد -> الذهاب لصفحة التسجيل
mySnackbarSuccess(
'Phone verified. Please complete registration.'.tr);
Get.to(() => RegistrationScreen(phoneNumber: phoneNumber));
}
} else {
mySnackeBarError(data['message']);
}
} else {
mySnackeBarError('Server error. Please try again.'.tr);
}
} catch (e) {
mySnackeBarError('An error occurred: $e');
}
}
static Future<void> _addTokens() async {
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
var res = await CRUD()
// this for register and login //
.post(link: "${AppLink.server}/ride/firebase/addToken.php", payload: {
'token': (box.read(BoxName.tokenFCM.toString())),
'passengerID': box.read(BoxName.passengerID).toString(),
"fingerPrint": fingerPrint
});
Log.print('res token: ${res}');
}
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']['data']);
} else {
mySnackeBarError(
"User with this phone number or email already exists.".tr);
}
} catch (e) {
Log.print('e: ${e}');
mySnackeBarError('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.name, userData['first_name']);
box.write(BoxName.lastName, userData['last_name']);
box.write(BoxName.email, userData['email']);
box.write(BoxName.phone, userData['phone']);
box.write(BoxName.isVerified, '1');
await _addTokens();
await Get.put(LoginController()).loginUsingCredentials(
box.read(BoxName.passengerID).toString(),
box.read(BoxName.email).toString(),
); // Navigate to home
}
}

View File

@@ -0,0 +1,367 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:siro_rider/constant/colors.dart';
import 'package:siro_rider/controller/auth/login_controller.dart';
import 'package:siro_rider/controller/functions/add_error.dart';
import 'package:siro_rider/controller/functions/encrypt_decrypt.dart';
import 'package:siro_rider/views/home/map_page_passenger.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_rider/constant/links.dart';
import 'package:siro_rider/constant/style.dart';
import 'package:siro_rider/controller/functions/crud.dart';
import 'package:siro_rider/views/auth/login_page.dart';
import 'package:siro_rider/views/widgets/elevated_btn.dart';
import '../../constant/box_name.dart';
import '../../main.dart';
import '../../print.dart';
import '../../views/auth/verify_email_page.dart';
import '../../views/widgets/mydialoug.dart';
import '../functions/sms_controller.dart';
class RegisterController extends GetxController {
final formKey = GlobalKey<FormState>();
final formKey3 = GlobalKey<FormState>();
TextEditingController firstNameController = TextEditingController();
TextEditingController lastNameController = TextEditingController();
TextEditingController emailController = TextEditingController();
TextEditingController phoneController = TextEditingController();
TextEditingController passwordController = TextEditingController();
TextEditingController siteController = TextEditingController();
// TextEditingController verfyCode = TextEditingController();
TextEditingController verifyCode = TextEditingController();
int remainingTime = 300; // 5 minutes in seconds
bool isSent = false;
bool isLoading = false;
Timer? _timer;
String birthDate = 'Birth Date'.tr;
String gender = 'Male'.tr;
@override
void onInit() {
super.onInit();
}
void startTimer() {
_timer?.cancel(); // Cancel any existing timer
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (remainingTime > 0) {
remainingTime--;
} else {
timer.cancel();
}
});
}
getBirthDate() {
Get.defaultDialog(
title: 'Select Date'.tr,
titleStyle: AppStyle.title,
content: SizedBox(
width: 300,
child: CalendarDatePicker(
initialDate:
DateTime.now().subtract(const Duration(days: 14 * 365)),
firstDate: DateTime.parse('1940-06-01'),
lastDate: DateTime.now().subtract(const Duration(days: 14 * 365)),
onDateChanged: (date) {
// Get the selected date and convert it to a DateTime object
DateTime dateTime = date;
// Call the getOrders() function from the controller
birthDate = dateTime.toString().split(' ')[0];
update();
},
// onDateChanged: (DateTime value) {},
),
),
confirm: MyElevatedButton(title: 'Ok'.tr, onPressed: () => Get.back()));
}
void changeGender(String value) {
gender = value;
update();
}
bool isValidEgyptianPhoneNumber(String phoneNumber) {
// Remove any whitespace from the phone number
phoneNumber = phoneNumber.replaceAll(RegExp(r'\s+'), '');
// Check if the phone number has exactly 11 digits
if (phoneNumber.length != 11) {
return false;
}
// Check if the phone number starts with 010, 011, 012, or 015
RegExp validPrefixes = RegExp(r'^01[0125]\d{8}$');
return validPrefixes.hasMatch(phoneNumber);
}
bool isValidPhoneNumber(String phoneNumber) {
// Remove any whitespace from the phone number
phoneNumber = phoneNumber.replaceAll(RegExp(r'\s+'), '');
// Check if the phone number has at least 10 digits
if (phoneNumber.length < 10) {
return false;
}
// Check for valid prefixes (modify this to match your use case)
RegExp validPrefixes = RegExp(r'^[0-9]+$');
// Check if the phone number contains only digits
return validPrefixes.hasMatch(phoneNumber);
}
sendOtpMessage() async {
SmsEgyptController smsEgyptController;
isLoading = true;
update();
try {
// Initialize SmsEgyptController
smsEgyptController = Get.put(SmsEgyptController());
isLoading = true;
update();
// Get phone number from controller
String phoneNumber = phoneController.text;
// Check if the phone number is from Egypt (Assuming Egyptian numbers start with +20)
if (phoneController.text.isNotEmpty) {
bool isEgyptianNumber = phoneNumber.startsWith('+20');
if (isEgyptianNumber && phoneNumber.length == 13) {
// Check if the phone number is already verified
var responseChecker = await CRUD().post(
link: AppLink.checkPhoneNumberISVerfiedPassenger,
payload: {
'phone_number': (phoneNumber),
'email': box.read(BoxName.email),
},
);
if (responseChecker != 'failure') {
var data = jsonDecode(responseChecker);
// If the phone number is already verified
if (data['message'][0]['verified'].toString() == '1') {
Get.snackbar('Phone number is verified before'.tr, '',
backgroundColor: AppColor.greenColor);
box.write(BoxName.isVerified, '1');
box.write(BoxName.phone, (phoneNumber));
Get.offAll(const MapPagePassenger());
} else {
await sendOtp(phoneNumber, isEgyptianNumber, smsEgyptController);
}
} else {
await sendOtp(phoneNumber, isEgyptianNumber, smsEgyptController);
}
} else if (phoneNumber.length > 9) {
sendOtp(phoneNumber, isEgyptianNumber, smsEgyptController);
}
} else {
MyDialog().getDialog(
'Error'.tr, 'Phone number must be exactly 11 digits long'.tr, () {
Get.back();
});
// sendOtp(
// phoneNumber, randomNumber, isEgyptianNumber, smsEgyptController);
}
} catch (e) {
// Handle error
} finally {
isLoading = false;
update();
}
}
// Helper function to send OTP or WhatsApp message based on phone number location
Future<void> sendOtp(String phoneNumber, bool isEgyptian,
SmsEgyptController controller) async {
// Trim any leading or trailing whitespace from the phone number
phoneNumber = phoneNumber.trim();
var dd = await CRUD().post(link: AppLink.sendVerifyOtpMessage, payload: {
'phone_number': (phoneNumber),
});
Log.print('dd: ${dd}');
// Common Registration Logic (extracted for reuse)
Future<void> registerUser() async {
await CRUD().post(link: AppLink.updatePhoneInvalidSMSPassenger, payload: {
"phone_number": (Get.find<RegisterController>().phoneController.text)
});
box.write(BoxName.phone, (phoneController.text));
var nameParts = (box.read(BoxName.name)).toString().split(' ');
var firstName = nameParts.isNotEmpty ? nameParts[0] : 'unknown';
var lastName = nameParts.length > 1 ? nameParts[1] : 'unknown';
var payload = {
'id': box.read(BoxName.passengerID),
'phone': (phoneController.text),
'email': box.read(BoxName.email),
'password':
('unknown'), //Consider if you *really* want to store 'unknown' passwords
'gender': ('unknown'),
'birthdate': ('2002-01-01'),
'site': box.read(BoxName.passengerPhotoUrl) ?? 'unknown',
'first_name': (firstName),
'last_name': (lastName),
};
var res1 = await CRUD().post(
link: AppLink.signUp,
payload: payload,
);
if (res1 != 'failure') {
//Multi-server signup (moved inside the successful registration check)
// if (AppLink.IntaleqAlexandriaServer != AppLink.IntaleqSyriaServer) {
// List<Future> signUp = [
// CRUD().post(
// link: '${AppLink.IntaleqAlexandriaServer}/auth/signup.php',
// payload: payload,
// ),
// CRUD().post(
// link: '${AppLink.IntaleqGizaServer}/auth/signup.php',
// payload: payload,
// )
// ];
// await Future.wait(signUp); // Wait for both sign-ups to complete.
// }
box.write(BoxName.isVerified, '1');
box.write(
BoxName.isFirstTime, '0'); //Double-check the logic for isFirstTime
box.write(BoxName.phone, (phoneController.text));
Get.put(LoginController()).loginUsingCredentials(
box.read(BoxName.passengerID).toString(),
box.read(BoxName.email).toString(),
);
}
}
if (isEgyptian) {
// verifySMSCode();
// await registerUser(); // Use the common registration logic
// await controller.sendSmsEgypt(phoneNumber, otp.toString()); // Optional: Send SMS if Egyptian
} else if (phoneController.text.toString().length >= 10) {
await registerUser(); // Use the common registration logic for non-Egyptian users as well.
// this for whatsapp messsage // Optional: Send WhatsApp message
// await CRUD().sendWhatsAppAuth(phoneNumber, otp.toString());
}
isLoading = false;
isSent = true;
remainingTime = 300;
update(); // Reset to 5 minutes
// startTimer(); // Consider whether you need a timer here, or if it's handled elsewhere.
}
verifySMSCode() async {
try {
if (formKey3.currentState!.validate()) {
var res = await CRUD().post(link: AppLink.verifyOtpPassenger, payload: {
'phone_number': phoneController.text,
'token': verifyCode.text,
});
if (res != 'failure') {
box.write(BoxName.phone, (phoneController.text));
var nameParts = (box.read(BoxName.name)).toString().split(' ');
var firstName = nameParts.isNotEmpty ? nameParts[0] : 'unknown';
var lastName = nameParts.length > 1 ? nameParts[1] : 'unknown';
var payload = {
'id': box.read(BoxName.passengerID),
'phone': (phoneController.text),
'email': box.read(BoxName.email),
'password': 'unknown',
'gender': 'unknown',
'birthdate': '2002-01-01',
'site': box.read(BoxName.passengerPhotoUrl) ?? 'unknown',
'first_name': firstName,
'last_name': lastName,
};
var res1 = await CRUD().post(
link: AppLink.signUp,
payload: payload,
);
if (res1 != 'failure') {
box.write(BoxName.isVerified, '1');
box.write(BoxName.isFirstTime, '0');
box.write(BoxName.phone, (phoneController.text));
Get.put(LoginController()).loginUsingCredentials(
box.read(BoxName.passengerID).toString(),
box.read(BoxName.email).toString(),
);
} else {
Get.snackbar('Error'.tr,
"The email or phone number is already registered.".tr,
backgroundColor: Colors.redAccent);
}
} else {
Get.snackbar('Error'.tr, "phone not verified".tr,
backgroundColor: Colors.redAccent);
}
} else {
Get.snackbar('Error'.tr, "you must insert token code".tr,
backgroundColor: AppColor.redColor);
}
} catch (e) {
addError(e.toString(), 'passenger sign up ');
Get.snackbar('Error'.tr, "Something went wrong. Please try again.".tr,
backgroundColor: Colors.redAccent);
}
}
sendVerifications() async {
var res = await CRUD().post(link: AppLink.verifyEmail, payload: {
'email': emailController.text,
'token': verifyCode.text,
});
var dec = jsonDecode(res);
if (dec['status'] == 'success') {
Get.offAll(() => LoginPage());
}
}
void register() async {
if (formKey.currentState!.validate()) {
var res = await CRUD().post(link: AppLink.signUp, payload: {
'first_name': firstNameController.text.toString(),
'last_name': lastNameController.text.toString(),
'email': emailController.text.toString(),
'phone': phoneController.text.toString(),
'password': passwordController.text.toString(),
'gender': 'yet',
'site': siteController.text,
'birthdate': birthDate,
});
if (jsonDecode(res)['status'] == 'success') {
int randomNumber = Random().nextInt(100000) + 1;
await CRUD().post(link: AppLink.sendVerifyEmail, payload: {
'email': emailController.text,
'token': randomNumber.toString(),
});
Get.to(() => const VerifyEmailPage());
}
}
}
@override
void onClose() {
_timer?.cancel();
super.onClose();
}
}

View File

@@ -0,0 +1,114 @@
import 'dart:async';
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/constant/links.dart';
import 'package:siro_rider/controller/functions/crud.dart';
import 'package:siro_rider/main.dart';
import 'package:get/get.dart';
import '../../print.dart';
import '../../views/home/map_page_passenger.dart';
import '../firebase/firbase_messge.dart';
import '../firebase/notification_service.dart';
import '../functions/package_info.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 {
if (isLoading.value) return;
isLoading.value = true;
try {
final response = await CRUD().post(
link: '${AppLink.server}/auth/token_passenger/send_otp.php',
payload: {
'receiver': phone,
// 'device_token': deviceToken,
},
);
if (response != 'failure') {
isLoading.value = true;
// بإمكانك عرض رسالة نجاح هنا
} else {
Get.snackbar('Error'.tr, 'Failed to send OTP'.tr);
}
} catch (e) {
Get.snackbar('Error', e.toString());
} finally {
// isLoading.value = false;
}
}
Future<void> verifyOtp(String ptoken) async {
isVerifying.value = true;
try {
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
final response = await CRUD().post(
link: '${AppLink.server}/auth/token_passenger/verify_otp.php',
payload: {
'phone_number': phone,
'otp': otpCode.value,
'token': box.read(BoxName.tokenFCM).toString(),
'fingerPrint': fingerPrint.toString(),
},
);
if (response != 'failure' && response['status'] == 'success') {
await NotificationService.sendNotification(
category: 'token change',
target: ptoken.toString(),
title: 'token change'.tr,
body: 'change device'.tr,
isTopic: false, // Important: this is a token
tone: 'cancel',
driverList: [],
);
Get.offAll(() => const MapPagePassenger());
} else {
Get.snackbar('Verification Failed', 'OTP is incorrect or expired');
}
} catch (e) {
Get.snackbar('Error', e.toString());
} finally {
isVerifying.value = false;
}
}
}

View File

@@ -0,0 +1,38 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import '../../constant/box_name.dart';
import '../../constant/links.dart';
import '../../main.dart';
import '../functions/encrypt_decrypt.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.') {
Get.snackbar('token updated'.tr, '');
}
}
}

View File

@@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_rider/constant/links.dart';
import 'package:siro_rider/controller/functions/crud.dart';
class VerifyEmailController extends GetxController {
TextEditingController verfyCode = TextEditingController();
@override
void onInit() async {
super.onInit();
}
sendverfications() async {
await CRUD().post(link: AppLink.sendVerifyEmail);
}
}

View File

@@ -0,0 +1,50 @@
import 'dart:convert';
import 'package:googleapis_auth/auth_io.dart';
class AccessTokenManager {
static final AccessTokenManager _instance = AccessTokenManager._internal();
late final String serviceAccountJsonKey;
AccessToken? _accessToken;
DateTime? _expiryDate;
AccessTokenManager._internal();
factory AccessTokenManager(String jsonKey) {
if (_instance._isServiceAccountKeyInitialized()) {
// Prevent re-initialization
return _instance;
}
_instance.serviceAccountJsonKey = jsonKey;
return _instance;
}
bool _isServiceAccountKeyInitialized() {
try {
serviceAccountJsonKey; // Access to check if initialized
return true;
} catch (e) {
return false;
}
}
Future<String> getAccessToken() async {
if (_accessToken != null && DateTime.now().isBefore(_expiryDate!)) {
return _accessToken!.data;
}
try {
final serviceAccountCredentials = ServiceAccountCredentials.fromJson(
json.decode(serviceAccountJsonKey));
final client = await clientViaServiceAccount(
serviceAccountCredentials,
['https://www.googleapis.com/auth/firebase.messaging'],
);
_accessToken = client.credentials.accessToken;
_expiryDate = client.credentials.accessToken.expiry;
client.close();
return _accessToken!.data;
} catch (e) {
throw Exception('Failed to obtain access token');
}
}
}

View File

@@ -0,0 +1,774 @@
import 'dart:convert';
import 'dart:io';
import 'package:siro_rider/views/home/HomePage/trip_monitor/trip_link_monitor.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_rider/controller/functions/toast.dart';
import 'package:siro_rider/views/widgets/elevated_btn.dart';
import '../../constant/box_name.dart';
import '../../constant/colors.dart';
import '../../constant/style.dart';
import '../../main.dart';
import '../../print.dart';
import '../../views/Rate/rate_captain.dart';
import '../../views/home/map_page_passenger.dart';
import '../../views/home/profile/promos_passenger_page.dart';
import '../auth/google_sign.dart';
import 'package:siro_rider/controller/voice_call_controller.dart';
import '../home/map/ride_lifecycle_controller.dart';
import '../home/map/ride_state.dart';
import 'local_notification.dart';
class FirebaseMessagesController extends GetxController {
final fcmToken = FirebaseMessaging.instance;
List<String> tokens = [];
List dataTokens = [];
late String driverID;
late String driverToken;
NotificationSettings? notificationSettings;
Future<void> getNotificationSettings() async {
// Get the current notification settings
NotificationSettings? notificationSettings =
await FirebaseMessaging.instance.getNotificationSettings();
'Notification authorization status: ${notificationSettings.authorizationStatus}';
// Call the update function if needed
update();
}
Future<void> requestFirebaseMessagingPermission() async {
FirebaseMessaging messaging = FirebaseMessaging.instance;
// Check if the platform is Android
if (Platform.isAndroid) {
// Request permission for Android
await messaging.requestPermission();
} else if (Platform.isIOS) {
// Request permission for iOS
NotificationSettings settings = await messaging.requestPermission(
alert: true,
announcement: true,
badge: true,
carPlay: true,
criticalAlert: true,
provisional: false,
sound: true,
);
messaging.setForegroundNotificationPresentationOptions(
alert: true, badge: true, sound: true);
}
}
NotificationController notificationController =
Get.isRegistered<NotificationController>()
? Get.find<NotificationController>()
: Get.put(NotificationController());
Future getToken() async {
fcmToken.getToken().then((token) {
// Log.print('fcmToken: ${token}');
box.write(BoxName.tokenFCM, (token.toString()));
});
// 🔹 الاشتراك في topic
await fcmToken
.subscribeToTopic("passengers"); // أو "users" حسب نوع المستخدم
Log.print("Subscribed to 'passengers' topic ✅");
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
// If the app is in the background or terminated, show a system tray message
RemoteNotification? notification = message.notification;
AndroidNotification? android = notification?.android;
// if (notification != null && android != null) {
if (message.data.isNotEmpty) {
fireBaseTitles(message);
}
});
FirebaseMessaging.onBackgroundMessage((RemoteMessage message) async {
// Handle background message
if (message.data.isNotEmpty) {
fireBaseTitles(message);
}
});
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
if (message.data.isNotEmpty && message.notification != null) {
fireBaseTitles(message);
}
});
}
Future<void> fireBaseTitles(RemoteMessage message) async {
// [!! تعديل !!]
// اقرأ "النوع" من حمولة البيانات، وليس من العنوان
String category = message.data['category'] ?? '';
final mapCtrl = Get.isRegistered<RideLifecycleController>()
? Get.find<RideLifecycleController>()
: null;
// اقرأ العنوان (للعرض)
String title = message.data['title'] ?? message.notification?.title ?? '';
String body = message.data['body'] ?? message.notification?.body ?? '';
if (category == 'ORDER') {
// <-- مثال: كان 'Order'.tr
Log.print('message: ${message}');
if (Platform.isAndroid) {
notificationController.showNotification(title, body, 'Order');
}
}
// ... داخل معالج الإشعارات في تطبيق الراكب ...
else if (category == 'Accepted Ride') {
if (mapCtrl != null) {
Map<String, dynamic>? driverInfoMap;
// 2. معالجة driver_info (تأتي كـ String JSON من PHP)
if (message.data['driver_info'] != null) {
try {
String rawJson = message.data['driver_info'];
// 🔥 فك التشفير: تحويل الـ String إلى Map
driverInfoMap = jsonDecode(rawJson);
} catch (e) {
Log.print("❌ Error decoding FCM driver_info: $e");
}
}
// 3. تمرير البيانات الجاهزة للكنترولر
await mapCtrl.processRideAcceptance(
driverData: driverInfoMap,
source: "FCM",
);
}
} else if (category == 'Promo') {
// <-- كان 'Promo'.tr
if (Platform.isAndroid) {
notificationController.showNotification(title, body, 'promo');
}
Get.to(const PromosPassengerPage());
} else if (category == 'Trip Monitoring') {
// <-- كان 'Trip Monitoring'.tr
if (Platform.isAndroid) {
notificationController.showNotification(title, body, 'iphone_ringtone');
}
var myListString = message.data['passengerList'];
var myList = jsonDecode(myListString) as List<dynamic>;
Get.to(() => TripMonitor(), arguments: {
'rideId': myList[0].toString(),
'driverId': myList[1].toString(),
});
} else if (category == 'token change') {
// <-- كان 'token change'.tr
if (Platform.isAndroid) {
notificationController.showNotification(title, body, 'cancel');
}
GoogleSignInHelper.signOut();
} else if (category == 'Driver Is Going To Passenger') {
// <-- كان 'Driver Is Going To Passenger'
Get.find<RideLifecycleController>().isDriverInPassengerWay = true;
Get.find<RideLifecycleController>().update();
if (Platform.isAndroid) {
notificationController.showNotification(title, body, 'tone1');
}
} else if (category == 'MSG_FROM_PASSENGER') {
// <-- كان 'message From passenger'
if (Platform.isAndroid) {
notificationController.showNotification(title, body, 'ding');
}
passengerDialog(body);
update();
} else if (category == 'message From Driver') {
// <-- كان 'message From Driver'
if (Platform.isAndroid) {
notificationController.showNotification(title, body, 'ding');
}
passengerDialog(body);
update();
} else if (category == 'Trip is Begin') {
// <-- كان 'Trip is Begin'
Log.print('[FCM] استقبل إشعار "TRIP_BEGUN".');
// استدعاء الحارس
mapCtrl!.processRideBegin(source: "FCM");
} else if (category == 'Hi ,I will go now') {
// <-- كان 'Hi ,I will go now'.tr
if (Platform.isAndroid) {
notificationController.showNotification(title, body, 'ding');
}
update();
} else if (category == "Arrive Ride") {
// استدعاء الحارس
mapCtrl!.processDriverArrival("FCM");
} else if (category == 'Driver Finish Trip') {
List<dynamic> driverList = [];
// ✅ معالجة آمنة للبيانات
var rawData = message.data['DriverList'];
if (rawData != null && rawData.isNotEmpty) {
try {
driverList = jsonDecode(rawData) as List<dynamic>;
} catch (e) {
Log.print("❌ Error decoding DriverList: $e");
}
}
if (driverList.isNotEmpty) {
Get.find<RideLifecycleController>()
.processRideFinished(driverList, source: "FCM");
}
} else if (category == 'Finish Monitor') {
// <-- كان "Finish Monitor".tr
Get.defaultDialog(
titleStyle: AppStyle.title,
title: 'Trip finished '.tr,
middleText: '',
confirm: MyElevatedButton(
title: 'Ok'.tr,
onPressed: () {
Get.offAll(() => const MapPagePassenger());
}));
} else if (category == 'Cancel Trip from driver') {
Log.print("🔔 FCM: Ride Cancelled by Driver received.");
// لا داعي لكتابة منطق التنظيف هنا، الكنترولر يتكفل بكل شيء
if (Get.isRegistered<RideLifecycleController>()) {
// استدعاء الحارس (سيتجاهل الأمر إذا كان السوكيت قد سبقه)
Get.find<RideLifecycleController>()
.processRideCancelledByDriver(message.data, source: "FCM");
}
// إشعار محلي (اختياري، لأن الديالوج سيظهر)
if (Platform.isAndroid) {
notificationController.showNotification(
'Trip Cancelled'.tr, 'The driver cancelled the trip.'.tr, 'cancel');
}
}
// ... (باقي الحالات مثل Call Income, Call End, إلخ) ...
// ... بنفس الطريقة ...
else if (category == 'incoming_call') {
final sessionId = message.data['session_id'];
final callerName = message.data['caller_name'];
final rideId = message.data['ride_id'];
if (sessionId != null && callerName != null && rideId != null) {
Get.find<VoiceCallController>().receiveCall(
sessionIdVal: sessionId.toString(),
remoteNameVal: callerName.toString(),
rideIdVal: rideId.toString(),
);
}
}
else if (category == 'Order Applied') {
if (Platform.isAndroid) {
notificationController.showNotification(
'The order Accepted by another Driver'.tr,
'We regret to inform you that another driver has accepted this order.'
.tr,
'order');
}
}
}
// Future<void> fireBaseTitles(RemoteMessage message) async {
// if (message.notification!.title! == 'Order'.tr) {
// Log.print('message: ${message}');
// if (Platform.isAndroid) {
// notificationController.showNotification(
// 'Order'.tr, message.notification!.body!, 'Order');
// }
// } else // ... داخل معالج الإشعارات في تطبيق الراكب ...
// if (message.notification!.title! == 'Accepted Ride') {
// // ...
// // انظر هنا: قمنا بتغيير "passengerList" إلى "driverList"
// var driverListJson = message.data['driverList'];
// // تأكد من أن البيانات ليست null قبل المتابعة
// if (driverListJson != null) {
// var myList = jsonDecode(driverListJson) as List<dynamic>;
// Log.print('myList: ${myList}');
// final controller = Get.find<RideLifecycleController>();
// // استدعاء الدالة الموحدة الجديدة التي أنشأناها
// await controller.processRideAcceptance(
// driverIdFromFCM: myList[0].toString(),
// rideIdFromFCM: myList[3].toString());
// } else {
// Log.print(
// '❌ خطأ فادح: إشعار "Accepted Ride" وصل بدون بيانات (driverList is null)');
// }
// } else if (message.notification!.title! == 'Promo'.tr) {
// if (Platform.isAndroid) {
// notificationController.showNotification(
// 'Promo', 'Show latest promo'.tr, 'promo');
// }
// Get.to(const PromosPassengerPage());
// } else if (message.notification!.title! == 'Trip Monitoring'.tr) {
// if (Platform.isAndroid) {
// notificationController.showNotification(
// 'Trip Monitoring'.tr, '', 'iphone_ringtone');
// }
// var myListString = message.data['DriverList'];
// var myList = jsonDecode(myListString) as List<dynamic>;
// Get.toNamed('/tripmonitor', arguments: {
// 'rideId': myList[0].toString(),
// 'driverId': myList[1].toString(),
// });
// } else if (message.notification!.title! == 'token change'.tr) {
// if (Platform.isAndroid) {
// notificationController.showNotification(
// 'token change'.tr, 'token change'.tr, 'cancel');
// }
// GoogleSignInHelper.signOut();
// } else if (message.notification!.title! == 'Driver Is Going To Passenger') {
// Get.find<RideLifecycleController>().isDriverInPassengerWay = true;
// Get.find<RideLifecycleController>().update();
// if (Platform.isAndroid) {
// notificationController.showNotification('Driver is Going To You'.tr,
// 'Please stay on the picked point.'.tr, 'tone1');
// }
// // Get.snackbar('Driver is Going To Passenger', '',
// // backgroundColor: AppColor.greenColor);
// } else if (message.notification!.title! == 'message From passenger') {
// if (Platform.isAndroid) {
// notificationController.showNotification(
// 'message From passenger'.tr, ''.tr, 'ding');
// }
// passengerDialog(message.notification!.body!);
// update();
// } else if (message.notification!.title! == 'message From Driver') {
// if (Platform.isAndroid) {
// notificationController.showNotification(
// 'message From Driver'.tr, ''.tr, 'ding');
// }
// passengerDialog(message.notification!.body!);
// update();
// } else // (هذا الكود في معالج الإشعارات لديك)
// if (message.notification!.title! == 'Trip is Begin') {
// Log.print('[FCM] استقبل إشعار "Trip is Begin".');
// // (تم حذف الإشعار المحلي من هنا، نُقل إلى الدالة الموحدة)
// final controller = Get.find<RideLifecycleController>();
// // استدعاء حارس البوابة الجديد والآمن
// controller.processRideBegin();
// // (تم حذف كل الأوامر التالية من هنا)
// // Get.find<RideLifecycleController>().getBeginRideFromDriver();
// // box.write(BoxName.passengerWalletTotal, '0');
// // update();
// } else if (message.notification!.title! == 'Hi ,I will go now'.tr) {
// // Get.snackbar('Hi ,I will go now', '',
// // backgroundColor: AppColor.greenColor);
// if (Platform.isAndroid) {
// notificationController.showNotification(
// 'Passenger come to you'.tr, 'Hi ,I will go now'.tr, 'ding');
// }
// update();
// } // ... داخل معالج الإشعارات (FCM Handler) ...
// if (message.notification!.title! == 'Hi ,I Arrive your site'.tr) {
// final controller = Get.find<RideLifecycleController>();
// // 1. التأكد أننا في الحالة الصحيحة (السائق كان في الطريق)
// if (controller.currentRideState.value == RideState.driverApplied) {
// Log.print('[FCM] السائق وصل. تغيير الحالة إلى driverArrived');
// // 2. تغيير الحالة فقط!
// controller.currentRideState.value = RideState.driverArrived;
// }
// } else if (message.notification!.title! == "Cancel Trip from driver") {
// Get.back();
// if (Platform.isAndroid) {
// notificationController.showNotification("Cancel Trip from driver".tr,
// "We will look for a new driver.\nPlease wait.".tr, 'cancel');
// }
// Get.defaultDialog(
// title: "The driver canceled your ride.".tr,
// middleText: "We will look for a new driver.\nPlease wait.".tr,
// confirm: MyElevatedButton(
// kolor: AppColor.greenColor,
// title: 'Ok'.tr,
// onPressed: () async {
// Get.back();
// await Get.find<RideLifecycleController>()
// .reSearchAfterCanceledFromDriver();
// },
// ),
// cancel: MyElevatedButton(
// title: 'Cancel'.tr,
// kolor: AppColor.redColor,
// onPressed: () {
// Get.offAll(() => const MapPagePassenger());
// },
// )
// // Get.find<RideLifecycleController>()
// // .searchNewDriverAfterRejectingFromDriver();
// );
// } else if (message.notification!.title! == 'Driver Finish Trip'.tr) {
// // الخطوة 1: استقبل البيانات وتحقق من وجودها
// final rawData = message.data['DriverList'];
// List<dynamic> driverList = []; // ابدأ بقائمة فارغة كإجراء وقائي
// // الخطوة 2: قم بفك تشفير البيانات بأمان
// if (rawData != null && rawData is String) {
// try {
// driverList = jsonDecode(rawData);
// Log.print('Successfully decoded DriverList: $driverList');
// } catch (e) {
// Log.print('Error decoding DriverList JSON: $e');
// // اترك القائمة فارغة في حالة حدوث خطأ
// }
// } else {
// Log.print('Error: DriverList data is null or not a String.');
// }
// // الخطوة 3: استخدم البيانات فقط إذا كانت القائمة تحتوي على العناصر المطلوبة
// // هذا يمنع خطأ "RangeError" إذا كانت القائمة أقصر من المتوقع
// if (driverList.length >= 4) {
// if (Platform.isAndroid) {
// notificationController.showNotification(
// "Driver Finish Trip".tr,
// '${'you will pay to Driver'.tr} ${driverList[3].toString()} \$', // تم تحسين طريقة عرض النص
// 'tone1');
// }
// Get.find<AudioRecorderController>().stopRecording();
// if ((double.tryParse(
// box.read(BoxName.passengerWalletTotal).toString()) ??
// 0) <
// 0) {
// box.write(BoxName.passengerWalletTotal, 0);
// }
// Get.find<RideLifecycleController>().tripFinishedFromDriver();
// NotificationController().showNotification(
// 'Dont forget your personal belongings.'.tr,
// 'Please make sure you have all your personal belongings and that any remaining fare, if applicable, has been added to your wallet before leaving. Thank you for choosing the Intaleq app'
// .tr,
// 'ding');
// Get.to(() => RateDriverFromPassenger(), arguments: {
// 'driverId': driverList[0].toString(),
// 'rideId': driverList[1].toString(),
// 'price': driverList[3].toString()
// });
// } else {
// Log.print(
// 'Error: Decoded driverList does not have enough elements. Received: $driverList');
// // هنا يمكنك عرض رسالة خطأ للمستخدم إذا لزم الأمر
// }
// } else if (message.notification!.title! == "Finish Monitor".tr) {
// Get.defaultDialog(
// titleStyle: AppStyle.title,
// title: 'Trip finished '.tr,
// middleText: '',
// confirm: MyElevatedButton(
// title: 'Ok'.tr,
// onPressed: () {
// Get.offAll(() => const MapPagePassenger());
// }));
// }
// // else if (message.notification!.title! == "Trip Monitoring".tr) {
// // Get.to(() => const TripMonitor());
// // }
// else if (message.notification!.title! == 'Call Income') {
// try {
// var myListString = message.data['DriverList'];
// var driverList = jsonDecode(myListString) as List<dynamic>;
// // if (Platform.isAndroid) {
// if (Platform.isAndroid) {
// notificationController.showNotification(
// 'Call Income'.tr,
// message.notification!.body!,
// 'iphone_ringtone',
// );
// }
// // }
// // Assuming GetMaterialApp is initialized and context is valid for navigation
// // Get.to(() => PassengerCallPage(
// // channelName: driverList[1].toString(),
// // token: driverList[0].toString(),
// // remoteID: driverList[2].toString(),
// // ));
// } catch (e) { Log.print("Error occurred: $e"); }
// } else if (message.notification!.title! == 'Call Income from Driver'.tr) {
// try {
// var myListString = message.data['DriverList'];
// var driverList = jsonDecode(myListString) as List<dynamic>;
// // if (Platform.isAndroid) {
// if (Platform.isAndroid) {
// notificationController.showNotification(
// 'Call Income'.tr,
// message.notification!.body!,
// 'iphone_ringtone',
// );
// }
// // Assuming GetMaterialApp is initialized and context is valid for navigation
// // Get.to(() => PassengerCallPage(
// // channelName: driverList[1].toString(),
// // token: driverList[0].toString(),
// // remoteID: driverList[2].toString(),
// // ));
// } catch (e) { Log.print("Error occurred: $e"); }
// } else if (message.notification!.title! == 'Call End'.tr) {
// try {
// var myListString = message.data['DriverList'];
// var driverList = jsonDecode(myListString) as List<dynamic>;
// if (Platform.isAndroid) {
// notificationController.showNotification(
// 'Call End'.tr,
// message.notification!.body!,
// 'ding',
// );
// }
// // Assuming GetMaterialApp is initialized and context is valid for navigation
// // Get.off(const CallPage());
// } catch (e) { Log.print("Error occurred: $e"); }
// } else if (message.notification!.title! == 'Driver Cancelled Your Trip') {
// // Get.snackbar(
// // 'You will be pay the cost to driver or we will get it from you on next trip'
// // .tr,
// // 'message',
// // backgroundColor: AppColor.redColor);
// if (Platform.isAndroid) {
// notificationController.showNotification(
// 'Driver Cancelled Your Trip'.tr,
// 'you will pay to Driver you will be pay the cost of driver time look to your Intaleq Wallet'
// .tr,
// 'cancel');
// }
// box.write(BoxName.parentTripSelected, false);
// box.remove(BoxName.tokenParent);
// Get.find<RideLifecycleController>().restCounter();
// Get.offAll(() => const MapPagePassenger());
// }
// // else if (message.notification!.title! == 'Order Applied') {
// // Get.snackbar(
// // "The order has been accepted by another driver."
// // .tr, // Corrected grammar
// // "Be more mindful next time to avoid dropping orders."
// // .tr, // Improved sentence structure
// // backgroundColor: AppColor.yellowColor,
// // snackPosition: SnackPosition.BOTTOM,
// // );
// // }
// else if (message.notification!.title! == 'Order Applied'.tr) {
// if (Platform.isAndroid) {
// notificationController.showNotification(
// 'The order Accepted by another Driver'.tr,
// 'We regret to inform you that another driver has accepted this order.'
// .tr,
// 'order');
// }
// }
// }
SnackbarController driverAppliedTripSnakBar() {
return Get.snackbar(
'Driver Applied the Ride for You'.tr,
'',
colorText: AppColor.greenColor,
duration: const Duration(seconds: 3),
snackPosition: SnackPosition.TOP,
titleText: Text(
'Applied'.tr,
style: TextStyle(color: AppColor.redColor),
),
messageText: Text(
'Driver Applied the Ride for You'.tr,
style: AppStyle.title,
),
icon: Icon(Icons.approval, color: AppColor.primaryColor),
shouldIconPulse: true,
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
);
}
Future<dynamic> passengerDialog(String message) {
return Get.defaultDialog(
barrierDismissible: false,
title: message.tr,
titleStyle: AppStyle.title,
middleTextStyle: AppStyle.title,
middleText: message.tr,
confirm: MyElevatedButton(
title: 'Ok'.tr,
onPressed: () {
// Get.find<FirebaseMessagesController>().sendNotificationToPassengerToken(
// 'Hi ,I will go now'.tr,
// 'I will go now'.tr,
// Get.find<RideLifecycleController>().driverToken, []);
// Get.find<RideLifecycleController>()
// .startTimerDriverWaitPassenger5Minute();
Get.back();
}));
}
Future<dynamic> driverFinishTripDialoge(List<dynamic> driverList) {
return Get.defaultDialog(
title: 'Driver Finish Trip'.tr,
content: const DriverTipWidget(),
confirm: MyElevatedButton(
title: 'Yes'.tr,
onPressed: () async {
Get.to(() => RateDriverFromPassenger(), arguments: {
'driverId': driverList[0].toString(),
'rideId': driverList[1].toString(),
'price': driverList[3].toString()
});
},
kolor: AppColor.greenColor,
),
cancel: MyElevatedButton(
title: 'No,I want'.tr,
onPressed: () {
Get.to(() => RateDriverFromPassenger(), arguments: {
'driverId': driverList[0].toString(),
'rideId': driverList[1].toString(),
'price': driverList[3].toString()
});
},
kolor: AppColor.redColor,
));
}
}
class DriverTipWidget extends StatelessWidget {
const DriverTipWidget({
super.key,
});
@override
Widget build(BuildContext context) {
return GetBuilder<RideLifecycleController>(builder: (controller) {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Text(
// '${'Your fee is '.tr}${Get.find<RideLifecycleController>().totalPassenger.toStringAsFixed(2)}'),
Text(
'Do you want to pay Tips for this Driver'.tr,
textAlign: TextAlign.center,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
InkWell(
onTap: () {
box.write(BoxName.tipPercentage, '0.05');
Toast.show(
context,
'${'Tip is '.tr}${(controller.totalPassenger) * (double.parse(box.read(BoxName.tipPercentage.toString())))}',
AppColor.cyanBlue);
controller.update();
},
child: Container(
decoration: BoxDecoration(border: Border.all()),
child: const Padding(
padding: EdgeInsets.all(4),
child: Center(
child: Text('5%'),
),
),
),
),
InkWell(
onTap: () {
box.write(BoxName.tipPercentage, '0.10');
Toast.show(
context,
'${'Tip is'.tr} ${(controller.totalPassenger) * (double.parse(box.read(BoxName.tipPercentage.toString())))}',
AppColor.cyanBlue);
controller.update();
},
child: Container(
decoration: BoxDecoration(border: Border.all()),
child: const Center(
child: Padding(
padding: EdgeInsets.all(5),
child: Text('10%'),
),
),
),
),
InkWell(
onTap: () {
box.write(BoxName.tipPercentage, '0.15');
Toast.show(
context,
'${'Tip is'.tr} ${(controller.totalPassenger) * (double.parse(box.read(BoxName.tipPercentage.toString())))}',
AppColor.cyanBlue);
controller.update();
},
child: Container(
decoration: BoxDecoration(border: Border.all()),
child: const Center(
child: Padding(
padding: EdgeInsets.all(5),
child: Text('15%'),
),
),
),
),
InkWell(
onTap: () {
box.write(BoxName.tipPercentage, '0.20');
Toast.show(
context,
'${'Tip is'.tr} ${(controller.totalPassenger) * (double.parse(box.read(BoxName.tipPercentage.toString())))}',
AppColor.cyanBlue);
controller.update();
},
child: Container(
decoration: BoxDecoration(border: Border.all()),
child: const Center(
child: Padding(
padding: EdgeInsets.all(5),
child: Text('20%'),
),
),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
MyElevatedButton(
kolor: AppColor.redColor,
title: 'No i want'.tr,
onPressed: () {
box.write(BoxName.tipPercentage, '0');
controller.update();
}),
Container(
decoration: AppStyle.boxDecoration1,
child: Padding(
padding: const EdgeInsets.all(6),
child: Text(
'${(controller.totalPassenger) * (double.parse(box.read(BoxName.tipPercentage.toString())))} ${box.read(BoxName.countryCode) == 'Egypt' ? 'LE'.tr : 'JOD'.tr}',
style: AppStyle.title,
),
),
),
],
)
],
);
});
}
}

View File

@@ -0,0 +1,60 @@
import 'package:siro_rider/print.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class LiveActivityScreen extends StatefulWidget {
@override
_LiveActivityScreenState createState() => _LiveActivityScreenState();
}
class _LiveActivityScreenState extends State<LiveActivityScreen> {
static const platform = MethodChannel('live_activity_channel');
Future<void> _startLiveActivity() async {
try {
await platform.invokeMethod('startLiveActivity');
} on PlatformException catch (e) {
Log.print("Failed to start Live Activity: '${e.message}'.");
}
}
Future<void> _updateLiveActivity(double progress) async {
try {
await platform.invokeMethod('updateLiveActivity', {"progress": progress});
} on PlatformException catch (e) {
Log.print("Failed to update Live Activity: '${e.message}'.");
}
}
Future<void> _endLiveActivity() async {
try {
await platform.invokeMethod('endLiveActivity');
} on PlatformException catch (e) {
Log.print("Failed to end Live Activity: '${e.message}'.");
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Live Activity Test")),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _startLiveActivity,
child: Text("Start Live Activity"),
),
ElevatedButton(
onPressed: () => _updateLiveActivity(0.5),
child: Text("Update Progress to 50%"),
),
ElevatedButton(
onPressed: _endLiveActivity,
child: Text("End Live Activity"),
),
],
),
);
}
}

View File

@@ -0,0 +1,336 @@
import 'package:siro_rider/print.dart';
import 'dart:async';
import 'dart:io';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:timezone/data/latest.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
import '../../main.dart';
class NotificationController extends GetxController {
final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
FlutterLocalNotificationsPlugin get plugin =>
_flutterLocalNotificationsPlugin;
@override
void onInit() {
super.onInit();
initNotifications();
}
// Initializes the local notifications plugin
Future<void> initNotifications() async {
const AndroidInitializationSettings android =
AndroidInitializationSettings('@mipmap/launcher_icon');
DarwinInitializationSettings ios = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
// onDidReceiveLocalNotification:
// (int id, String? title, String? body, String? payload) async {},
);
InitializationSettings initializationSettings =
InitializationSettings(android: android, iOS: ios);
await _flutterLocalNotificationsPlugin.initialize(
settings: initializationSettings);
tz.initializeTimeZones();
Log.print('Notifications initialized');
}
// Displays a notification with the given title and message
void showNotification(String title, String message, String tone) async {
final AndroidNotificationDetails android = AndroidNotificationDetails(
'high_importance_channel',
'High Importance Notifications',
importance: Importance.max,
priority: Priority.high,
showWhen: false,
sound: RawResourceAndroidNotificationSound(tone),
);
const DarwinNotificationDetails ios = DarwinNotificationDetails(
sound: 'default',
presentAlert: true,
presentBadge: true,
presentSound: true,
);
final NotificationDetails details =
NotificationDetails(android: android, iOS: ios);
await _flutterLocalNotificationsPlugin.show(
id: 0, title: title, body: message, notificationDetails: details);
Log.print('Notification shown: $title - $message');
}
// /Users/hamzaaleghwairyeen/development/App/ride 2/lib/controller/firebase/local_notification.dart
// Assume _flutterLocalNotificationsPlugin is initialized somewhere in your code
// void scheduleNotificationsForSevenDays(
// String title, String message, String tone) async {
// final AndroidNotificationDetails android = AndroidNotificationDetails(
// 'high_importance_channel',
// 'High Importance Notifications',
// importance: Importance.max,
// priority: Priority.high,
// sound: RawResourceAndroidNotificationSound(tone),
// );
// const DarwinNotificationDetails ios = DarwinNotificationDetails(
// sound: 'default',
// presentAlert: true,
// presentBadge: true,
// presentSound: true,
// );
// final NotificationDetails details =
// NotificationDetails(android: android, iOS: ios);
// // Check for the exact alarm permission on Android 12 and above
// if (Platform.isAndroid) {
// if (await Permission.scheduleExactAlarm.isDenied) {
// if (await Permission.scheduleExactAlarm.request().isGranted) {
// Log.print('SCHEDULE_EXACT_ALARM permission granted');
// } else {
// Log.print('SCHEDULE_EXACT_ALARM permission denied');
// return;
// }
// }
// }
// // Schedule notifications for the next 7 days
// for (int day = 0; day < 7; day++) {
// // Schedule for 8:00 AM
// await _scheduleNotificationForTime(
// day, 8, 0, title, message, details, day * 1000 + 1);
// // Schedule for 3:00 PM
// await _scheduleNotificationForTime(
// day, 15, 0, title, message, details, day * 1000 + 2); // Unique ID
// // Schedule for 8:00 PM
// await _scheduleNotificationForTime(
// day, 20, 0, title, message, details, day * 1000 + 3); // Unique ID
// }
// Log.print('Notifications scheduled successfully for the next 7 days');
// }
void scheduleNotificationsForSevenDays(
String title, String message, String tone) async {
final AndroidNotificationDetails android = AndroidNotificationDetails(
'high_importance_channel',
'High Importance Notifications',
importance: Importance.max,
priority: Priority.high,
sound: RawResourceAndroidNotificationSound(tone),
);
const DarwinNotificationDetails ios = DarwinNotificationDetails(
sound: 'default',
presentAlert: true,
presentBadge: true,
presentSound: true,
);
final NotificationDetails details =
NotificationDetails(android: android, iOS: ios);
// Check for the exact alarm permission on Android 12 and above
if (Platform.isAndroid) {
if (await Permission.scheduleExactAlarm.isDenied) {
if (await Permission.scheduleExactAlarm.request().isGranted) {
Log.print('SCHEDULE_EXACT_ALARM permission granted');
} else {
Log.print('SCHEDULE_EXACT_ALARM permission denied');
return;
}
}
}
// Schedule notifications for the next 7 days
for (int day = 0; day < 7; day++) {
// List of notification times
final notificationTimes = [
{'hour': 8, 'minute': 0, 'id': day * 1000 + 1}, // 8:00 AM
{'hour': 15, 'minute': 0, 'id': day * 1000 + 2}, // 3:00 PM
{'hour': 20, 'minute': 0, 'id': day * 1000 + 3}, // 8:00 PM
];
for (var time in notificationTimes) {
final notificationId = time['id'] as int;
// Check if this notification ID is already stored
bool isScheduled = box.read('notification_$notificationId') ?? false;
if (!isScheduled) {
// Schedule the notification if not already scheduled
await _scheduleNotificationForTime(
day,
time['hour'] as int,
time['minute'] as int,
title,
message,
details,
notificationId,
);
// Mark this notification ID as scheduled in GetStorage
box.write('notification_$notificationId', true);
} else {
Log.print('Notification with ID $notificationId is already scheduled.');
}
}
}
Log.print('Notifications scheduled successfully for the next 7 days');
}
void scheduleNotificationsForTimeSelected(
String title, String message, String tone, DateTime timeSelected) async {
final AndroidNotificationDetails android = AndroidNotificationDetails(
'high_importance_channel',
'High Importance Notifications',
importance: Importance.max,
priority: Priority.high,
sound: RawResourceAndroidNotificationSound(tone),
);
const DarwinNotificationDetails ios = DarwinNotificationDetails(
sound: 'default',
presentAlert: true,
presentBadge: true,
presentSound: true,
);
final NotificationDetails details =
NotificationDetails(android: android, iOS: ios);
// Check for the exact alarm permission on Android 12 and above
if (Platform.isAndroid) {
if (await Permission.scheduleExactAlarm.isDenied) {
if (await Permission.scheduleExactAlarm.request().isGranted) {
Log.print('SCHEDULE_EXACT_ALARM permission granted');
} else {
Log.print('SCHEDULE_EXACT_ALARM permission denied');
return;
}
}
}
// Schedule notifications for 10 and 30 minutes before the timeSelected
await _scheduleNotificationForTimeVIP(
timeSelected.subtract(const Duration(minutes: 10)), // 10 minutes before
title,
message,
details,
1, // Unique ID for 10-minute before notification
);
await _scheduleNotificationForTimeVIP(
timeSelected.subtract(const Duration(minutes: 30)), // 30 minutes before
title,
message,
details,
2, // Unique ID for 30-minute before notification
);
Log.print('Notifications scheduled successfully for the time selected');
}
Future<void> _scheduleNotificationForTimeVIP(
DateTime scheduledDate,
String title,
String message,
NotificationDetails details,
int notificationId,
) async {
// Initialize and set Cairo timezone
tz.initializeTimeZones();
var cairoLocation = tz.getLocation('Africa/Cairo');
final now = tz.TZDateTime.now(cairoLocation);
// Convert to Cairo time
tz.TZDateTime scheduledTZDateTime =
tz.TZDateTime.from(scheduledDate, cairoLocation);
// Check if 10 minutes before the scheduled time is in the past
if (scheduledTZDateTime
.subtract(const Duration(minutes: 10))
.isBefore(now)) {
// If the 10 minutes before the scheduled time is in the past, don't schedule
Log.print(
'Scheduled time minus 10 minutes is in the past. Skipping notification.');
return; // Skip this notification
}
Log.print('Current time (Cairo): $now');
Log.print('Scheduling notification for: $scheduledTZDateTime');
await _flutterLocalNotificationsPlugin.zonedSchedule(
id: notificationId, // Unique ID for each notification
title: title,
body: message,
scheduledDate: scheduledTZDateTime,
notificationDetails: details,
androidScheduleMode: AndroidScheduleMode.exact,
// uiLocalNotificationDateInterpretation:
// UILocalNotificationDateInterpretation.absoluteTime,
matchDateTimeComponents:
null, // Don't repeat automatically; we handle manually
);
Log.print('Notification scheduled successfully for: $scheduledTZDateTime');
}
Future<void> _scheduleNotificationForTime(
int dayOffset,
int hour,
int minute,
String title,
String message,
NotificationDetails details,
int notificationId,
) async {
// Initialize and set Cairo timezone
tz.initializeTimeZones();
var cairoLocation = tz.getLocation('Africa/Cairo');
final now = tz.TZDateTime.now(cairoLocation);
tz.TZDateTime scheduledDate = tz.TZDateTime(
cairoLocation,
now.year,
now.month,
now.day + dayOffset, // Add offset to schedule for the next days
hour,
minute,
);
// If the scheduled time is in the past, move it to the next day
if (scheduledDate.isBefore(now)) {
scheduledDate = scheduledDate.add(const Duration(days: 1));
}
Log.print('Current time (Cairo): $now');
Log.print('Scheduling notification for: $scheduledDate');
await _flutterLocalNotificationsPlugin.zonedSchedule(
id: notificationId, // Unique ID for each notification
title: title,
body: message,
scheduledDate: scheduledDate,
notificationDetails: details,
androidScheduleMode: AndroidScheduleMode.exact,
// uiLocalNotificationDateInterpretation:
// UILocalNotificationDateInterpretation.absoluteTime,
matchDateTimeComponents:
null, // Don't repeat automatically; we handle 7 days manually
);
Log.print('Notification scheduled successfully for: $scheduledDate');
}
}

View File

@@ -0,0 +1,66 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:get/get.dart';
import '../../print.dart'; // للترجمة .tr
class NotificationService {
static const String _serverUrl =
'https://api.intaleq.xyz/intaleq/ride/firebase/send_fcm.php';
static Future<void> sendNotification({
required String target,
required String title,
required String body,
required String category, // إلزامي للتصنيف
String? tone,
List<String>? driverList,
bool isTopic = false,
}) async {
try {
// 1. تجهيز البيانات المخصصة (Data Payload)
Map<String, dynamic> customData = {};
customData['category'] = category;
// إذا كان هناك قائمة سائقين/ركاب، نضعها هنا
if (driverList != null && driverList.isNotEmpty) {
// نرسلها كـ JSON String لأن FCM v1 يدعم String Values فقط في الـ data
customData['driverList'] = jsonEncode(driverList);
}
// 2. تجهيز الطلب الرئيسي للسيرفر
final Map<String, dynamic> requestPayload = {
'target': target,
'title': title,
'body': body,
'isTopic': isTopic,
'data':
customData, // 🔥🔥 التغيير الجوهري: وضعنا البيانات داخل "data" 🔥🔥
};
if (tone != null) {
requestPayload['tone'] = tone;
}
final response = await http.post(
Uri.parse(_serverUrl),
headers: {
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(requestPayload),
);
if (response.statusCode == 200) {
Log.print('✅ Notification sent successfully.');
// Log.print('Response: ${response.body}');
} else {
Log.print(
'❌ Failed to send notification. Code: ${response.statusCode}');
Log.print('Error Body: ${response.body}');
}
} catch (e) {
Log.print('❌ Error sending notification: $e');
}
}
}

View File

@@ -0,0 +1,19 @@
import '../../constant/box_name.dart';
import '../../constant/links.dart';
import '../../main.dart';
import 'crud.dart';
addError(String error, where) async {
CRUD().post(link: AppLink.addError, payload: {
'error': error.toString(), // Example error description
'userId': box.read(BoxName.driverID) ??
box.read(BoxName.passengerID), // Example user ID
'userType': box.read(BoxName.driverID) != null
? 'Driver'
: 'passenger', // Example user type
'phone': box.read(BoxName.phone) ??
box.read(BoxName.phoneDriver), // Example phone number
'device': where
});
}

View File

@@ -0,0 +1,169 @@
import 'package:siro_rider/print.dart';
import 'dart:io';
// import 'package:flutter_sound/flutter_sound.dart';
import 'package:path_provider/path_provider.dart';
import 'package:get/get.dart';
import 'package:just_audio/just_audio.dart';
import 'package:record/record.dart';
class AudioRecorderController extends GetxController {
AudioPlayer audioPlayer = AudioPlayer();
AudioRecorder recorder = AudioRecorder();
bool isRecording = false;
bool isPlaying = false;
bool isPaused = false;
String filePath = '';
String? selectedFilePath;
double currentPosition = 0;
double totalDuration = 0;
Future<void> playSoundFromAssets(String sound) async {
try {
await audioPlayer.setAsset(sound);
audioPlayer.play();
} catch (e) {
Log.print("Error playing sound: $e");
}
}
// Start recording
Future<void> startRecording({String? rideId}) async {
final bool isPermissionGranted = await recorder.hasPermission();
if (!isPermissionGranted) {
// RecordingPermissionException('l');
return;
}
final directory = await getApplicationDocumentsDirectory();
final String dateStr =
'${DateTime.now().year}-${DateTime.now().month.toString().padLeft(2, '0')}-${DateTime.now().day.toString().padLeft(2, '0')}';
// Generate a unique file name using the current timestamp
String fileName = (rideId != null && rideId.isNotEmpty && rideId != 'yet' && rideId != 'null')
? '${dateStr}_$rideId.m4a'
: '$dateStr.m4a';
filePath = '${directory.path}/$fileName';
// Define the configuration for the recording
const config = RecordConfig(
// Specify the format, encoder, sample rate, etc., as needed
encoder: AudioEncoder.aacLc, // For example, using AAC codec
sampleRate: 44100, // Sample rate
bitRate: 128000, // Bit rate
);
// Start recording to file with the specified configuration
await recorder.start(config, path: filePath);
isRecording = true;
update();
}
// Pause recording
Future<void> pauseRecording() async {
if (isRecording && !isPaused) {
await recorder.pause();
isPaused = true;
update();
}
}
// Resume recording
Future<void> resumeRecording() async {
if (isRecording && isPaused) {
await recorder.resume();
isPaused = false;
update();
}
}
// Stop recording
Future<void> stopRecording() async {
recorder.stop();
isRecording = false;
isPaused = false;
update();
}
// Play the selected recorded file
Future<void> playRecordedFile(String filePath) async {
await audioPlayer.setFilePath(filePath);
totalDuration = audioPlayer.duration?.inSeconds.toDouble() ?? 0;
audioPlayer.play();
isPlaying = true;
isPaused = false;
audioPlayer.positionStream.listen((position) {
currentPosition = position.inSeconds.toDouble();
update();
});
selectedFilePath = filePath;
update();
}
// Pause playback
Future<void> pausePlayback() async {
if (isPlaying && !isPaused) {
await audioPlayer.pause();
isPaused = true;
update();
}
}
// Resume playback
Future<void> resumePlayback() async {
if (isPlaying && isPaused) {
await audioPlayer.play();
isPaused = false;
update();
}
}
// Stop playback
Future<void> stopPlayback() async {
await audioPlayer.stop();
isPlaying = false;
isPaused = false;
currentPosition = 0;
update();
}
// Get a list of recorded files
Future<List<String>> getRecordedFiles() async {
final directory = await getApplicationDocumentsDirectory();
final files = await directory.list().toList();
return files
.map((file) => file.path)
.where((path) => path.endsWith('.m4a'))
.toList();
}
// Delete a specific recorded file
Future<void> deleteRecordedFile(String filePath) async {
final file = File(filePath);
if (await file.exists()) {
await file.delete();
update();
}
}
// Delete all recorded files
Future<void> deleteAllRecordedFiles() async {
final directory = await getApplicationDocumentsDirectory();
final files = await directory.list().toList();
for (final file in files) {
if (file.path.endsWith('.m4a')) {
await deleteRecordedFile(file.path);
}
}
}
@override
void onClose() {
audioPlayer.dispose();
recorder.dispose();
super.onClose();
}
}

View File

@@ -0,0 +1,134 @@
// import 'package:SEFER/constant/api_key.dart';
// import 'package:SEFER/controller/functions/crud.dart';
// import 'package:SEFER/controller/home/map_passenger_controller.dart';
// import 'package:agora_rtc_engine/agora_rtc_engine.dart';
// import 'package:get/get.dart';
// import 'package:permission_handler/permission_handler.dart';
// import '../../constant/box_name.dart';
// import '../firebase/firbase_messge.dart';
// import '../../main.dart';
// class CallController extends GetxController {
// String channelName = ''; // Get.find<MapDriverController>().rideId;
// String token = '';
// // int uid = int.parse(box.read(BoxName.phoneDriver)); // uid of the local user
// int uid = 0;
// int? remoteUid; // uid of the remote user
// bool _isJoined = false; // Indicates if the local user has joined the channel
// String status = '';
// late RtcEngine agoraEngine; // Agora engine instance
// @override
// void onInit() {
// super.onInit();
// // if (box.read(BoxName.passengerID) != null) {
// channelName = Get.find<MapPassengerController>().rideId; // 'sefer300'; //
// remoteUid = int.parse(Get.find<MapPassengerController>().driverPhone);
// uid = int.parse(box.read(BoxName.phone));
// // } else {
// // channelName = Get.find<MapDriverController>().rideId; // 'sefer300'; //
// // remoteUid = int.parse(Get.find<MapDriverController>().passengerPhone);
// // uid = int.parse(box.read(BoxName.phoneDriver));
// // }
// initAgoraFull();
// }
// initAgoraFull() async {
// await fetchToken();
// // Set up an instance of Agora engine
// setupVoiceSDKEngine();
// // join();
// FirebaseMessagesController().sendNotificationToPassengerToken(
// 'Call Income from Passenger',
// '${'You have call from Passenger'.tr} ${box.read(BoxName.name)}',
// Get.find<MapPassengerController>().driverToken,
// [
// token,
// channelName,
// uid.toString(),
// remoteUid.toString(),
// ],
// 'iphone_ringtone.wav',
// );
// join();
// }
// @override
// void onClose() {
// agoraEngine.leaveChannel();
// super.onClose();
// }
// Future<void> setupVoiceSDKEngine() async {
// // retrieve or request microphone permission
// await [Permission.microphone].request();
// //create an instance of the Agora engine
// agoraEngine = createAgoraRtcEngine();
// await agoraEngine.initialize(RtcEngineContext(appId: AK.agoraAppId));
// // Register the event handler
// agoraEngine.registerEventHandler(
// RtcEngineEventHandler(
// onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
// // Get.snackbar(
// // "Local user uid:${connection.localUid} joined the channel", '');
// status = 'joined'.tr;
// _isJoined = true;
// update();
// },
// onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {
// // Get.snackbar("Remote user uid:$remoteUid joined the channel", '');
// status = Get.find<MapPassengerController>().driverName.toString();
// ' joined'.tr;
// remoteUid = remoteUid;
// update();
// },
// onUserOffline: (RtcConnection connection, int? remoteUid,
// UserOfflineReasonType reason) {
// // Get.snackbar("Remote user uid:$remoteUid left the channel", '');
// status = 'Call left'.tr;
// remoteUid = null;
// update();
// },
// ),
// );
// }
// void join() async {
// // Set channel options including the client role and channel profile
// ChannelMediaOptions options = const ChannelMediaOptions(
// clientRoleType: ClientRoleType.clientRoleBroadcaster,
// channelProfile: ChannelProfileType.channelProfileCommunication,
// );
// await agoraEngine.joinChannel(
// token: token,
// channelId: channelName,
// options: options,
// uid: uid,
// );
// }
// void leave() {
// _isJoined = false;
// remoteUid = null;
// update();
// agoraEngine.leaveChannel();
// }
// // Clean up the resources when you leave
// @override
// void dispose() async {
// await agoraEngine.leaveChannel();
// super.dispose();
// }
// fetchToken() async {
// var res = await CRUD()
// .getAgoraToken(channelName: channelName, uid: uid.toString());
// token = res;
// update();
// }
// }

View File

@@ -0,0 +1,720 @@
import 'dart:async';
import 'dart:convert';
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/constant/links.dart';
import 'package:siro_rider/controller/auth/login_controller.dart';
import 'package:siro_rider/main.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import 'package:siro_rider/env/env.dart';
import '../../constant/api_key.dart';
import '../../print.dart';
import '../../views/widgets/elevated_btn.dart';
import '../../views/widgets/error_snakbar.dart';
import 'encrypt_decrypt.dart';
import 'upload_image.dart';
import 'dart:io';
import 'network/net_guard.dart';
class CRUD {
final NetGuard _netGuard = NetGuard();
final _client = http.Client();
/// Stores the signature of the last logged error to prevent duplicates.
static String _lastErrorSignature = '';
/// Stores the timestamp of the last logged error.
static DateTime _lastErrorTimestamp = DateTime(2000);
/// The minimum time that must pass before logging the same error again.
static const Duration _errorLogDebounceDuration = Duration(minutes: 1);
/// Asynchronously logs an error to the server with debouncing to prevent log flooding.
static Future<void> addError(
String error, String details, String where) async {
try {
final currentErrorSignature = '$where-$error';
final now = DateTime.now();
if (currentErrorSignature == _lastErrorSignature &&
now.difference(_lastErrorTimestamp) < _errorLogDebounceDuration) {
return;
}
_lastErrorSignature = currentErrorSignature;
_lastErrorTimestamp = now;
final userId =
box.read(BoxName.driverID) ?? box.read(BoxName.passengerID);
final userType =
box.read(BoxName.driverID) != null ? 'Driver' : 'Passenger';
final phone = box.read(BoxName.phone) ?? box.read(BoxName.phoneDriver);
// طباعة الخطأ في الكونسول للمطور للمتابعة الفورية
Log.print(
"🚨 [ADD_ERROR] Where: $where | Error: $error | Details: $details");
// Fire-and-forget call to prevent infinite loops if the logger itself fails.
CRUD().post(
link: AppLink.addError,
payload: {
'error': error.toString(),
'userId': userId.toString(),
'userType': userType,
'phone': phone.toString(),
'device': where,
'details': details,
},
);
} catch (e) {
Log.print("Error occurred: $e");
}
}
// ─────────────────────────────────────────────────────────────
// دالة مساعدة خاصة: يجيب البصمة المشفرة من GetStorage
// هي نفس القيمة المرسلة في login وعُملها hash في JWT payload
// السيرفر يعمل: sha256(X-Device-FP + FP_PEPPER) == JWT.fingerPrint
// ─────────────────────────────────────────────────────────────
String _getFpHeader() {
return box.read(BoxName.deviceFpEncrypted)?.toString() ?? '';
}
/// Centralized private method to handle all API requests.
/// Includes retry logic, network checking, and standardized error handling.
Future<dynamic> _makeRequest({
required String link,
Map<String, dynamic>? payload,
required Map<String, String> headers,
}) async {
const connectTimeout = Duration(seconds: 6);
const receiveTimeout = Duration(seconds: 10);
Future<http.Response> doPost() {
final url = Uri.parse(link);
return _client
.post(url, body: payload, headers: headers)
.timeout(connectTimeout + receiveTimeout);
}
http.Response response;
try {
// retry ذكي: محاولة واحدة إضافية فقط لأخطاء شبكة/5xx
try {
response = await doPost();
} on SocketException catch (_) {
response = await doPost();
} on TimeoutException catch (_) {
response = await doPost();
}
final sc = response.statusCode;
final body = response.body;
Log.print('request: ${response.request}');
Log.print('body: $body');
// Log.print('link: $link');
Log.print('headers: $headers');
Log.print('payload: $payload');
// 2xx
if (sc >= 200 && sc < 300) {
try {
final jsonData = jsonDecode(body);
return jsonData;
} catch (e, st) {
addError('JSON Decode Error', 'Body: $body\n$st',
'CRUD._makeRequest $link');
return 'failure';
}
}
// 401 → تجديد التوكن تلقائياً
if (sc == 401) {
await Get.put(LoginController()).getJWT();
return 'token_expired';
}
// 5xx
if (sc >= 500) {
addError(
'Server 5xx', 'SC: $sc\nBody: $body', 'CRUD._makeRequest $link');
return 'failure';
}
// 4xx أخرى
return 'failure';
} on SocketException {
_netGuard.notifyOnce((title, msg) => mySnackeBarError(msg));
return 'no_internet';
} on TimeoutException {
return 'failure';
} catch (e, st) {
addError('HTTP Request Exception: $e', 'Stack: $st',
'CRUD._makeRequest $link');
return 'failure';
}
}
// ═══════════════════════════════════════════════════════════════
// post — طلب POST عادي للراكب/السائق
// ───────────────────────────────────────────────────────────────
// التغيير: إضافة X-Device-FP header
// القيمة: fp_encrypted من GetStorage
// السيرفر يتحقق: sha256(fp_encrypted + FP_PEPPER) == JWT.fingerPrint
// ═══════════════════════════════════════════════════════════════
Future<dynamic> post({
required String link,
Map<String, dynamic>? payload,
}) async {
String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
final headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer $token',
'X-Device-FP': _getFpHeader(), // ← إثبات الجهاز
};
return await _makeRequest(
link: link,
payload: payload,
headers: headers,
);
}
// ═══════════════════════════════════════════════════════════════
// get — طلب GET للراكب/السائق (يستخدم POST method)
// ───────────────────────────────────────────────────────────────
// التغيير: إضافة X-Device-FP header
// ═══════════════════════════════════════════════════════════════
Future<dynamic> get({
required String link,
Map<String, dynamic>? payload,
}) async {
var url = Uri.parse(link);
var response = await http.post(
url,
body: payload,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization':
'Bearer ${r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]}',
'X-Device-FP': _getFpHeader(), // ← إثبات الجهاز
},
);
Log.print('request: ${response.request}');
Log.print('body: ${response.body}');
Log.print('payload: $payload');
if (response.statusCode == 200) {
return response.body;
} else if (response.statusCode == 401) {
var jsonData = jsonDecode(response.body);
if (jsonData['error'] == 'Token expired') {
print("CRUD.get: Token expired, refreshing and retrying once...");
await Get.put(LoginController()).getJWT();
// إعادة المحاولة مرة واحدة فقط بتوكن جديد
var retryResponse = await http.post(
url,
body: payload,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization':
'Bearer ${r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]}',
'X-Device-FP': _getFpHeader(),
},
);
if (retryResponse.statusCode == 200) {
return retryResponse.body;
}
return jsonEncode(
{'status': 'failure', 'message': 'token_expired_retry_failed'});
} else {
return jsonEncode({'status': 'failure', 'message': '401_unauthorized'});
}
} else {
addError('Non-200 response code: ${response.statusCode}',
'crud().get - Other', url.toString());
return jsonEncode({
'status': 'failure',
'message': 'server_error_${response.statusCode}'
});
}
}
// ═══════════════════════════════════════════════════════════════
// postWallet — طلب POST لسيرفر المدفوعات
// ───────────────────────────────────────────────────────────────
// التغيير: إضافة X-Device-FP header
// 3 headers معاً: JWT + HMAC + FP
// ═══════════════════════════════════════════════════════════════
Future<dynamic> postWallet({
required String link,
Map<String, dynamic>? payload,
}) async {
var jwt = await LoginController().getJwtWallet();
final hmac = box.read(BoxName.hmac);
final headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer $jwt',
'X-HMAC-Auth': hmac.toString(),
'X-Device-FP': _getFpHeader(), // ← إثبات الجهاز
};
// add print debug
Log.print('headers: $headers');
Log.print('payload: $payload');
Log.print('link: $link');
return await _makeRequest(
link: link,
payload: payload,
headers: headers,
);
}
// ═══════════════════════════════════════════════════════════════
// getWallet — طلب GET لسيرفر المدفوعات (يستخدم POST method)
// ───────────────────────────────────────────────────────────────
// التغيير: إضافة X-Device-FP header
// ═══════════════════════════════════════════════════════════════
Future<dynamic> getWallet({
required String link,
Map<String, dynamic>? payload,
}) async {
var s = await LoginController().getJwtWallet();
final hmac = box.read(BoxName.hmac);
var url = Uri.parse(link);
var response = await http.post(
url,
body: payload,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer $s',
'X-HMAC-Auth': hmac.toString(),
'X-Device-FP': _getFpHeader(), // ← إثبات الجهاز
},
);
if (response.statusCode == 200) {
var jsonData = jsonDecode(response.body);
if (jsonData['status'] == 'success') {
return response.body;
}
return jsonData['status'];
} else if (response.statusCode == 401) {
var jsonData = jsonDecode(response.body);
if (jsonData['error'] == 'Token expired') {
await Get.put(LoginController()).getJwtWallet();
return 'token_expired';
} else {
addError('Unauthorized: ${jsonData['error']}', 'crud().getWallet - 401',
url.toString());
return 'failure';
}
} else {
addError('Non-200 response code: ${response.statusCode}',
'crud().getWallet - Other', url.toString());
return 'failure';
}
}
// =======================================================================
// All other specialized methods remain below unchanged.
// They interact with external third-party APIs and have unique
// authentication or body structures that don't need the FP header.
// =======================================================================
Future<dynamic> postWalletMtn(
{required String link, Map<String, dynamic>? payload}) async {
final s = await LoginController().getJwtWallet();
final hmac = box.read(BoxName.hmac);
final url = Uri.parse(link);
try {
final response = await http.post(
url,
body: payload,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer $s',
'X-HMAC-Auth': hmac.toString(),
'X-Device-FP': _getFpHeader(), // ← إثبات الجهاز
},
);
Map<String, dynamic> wrap(String status, {Object? message, int? code}) {
return {
'status': status,
'message': message,
'code': code ?? response.statusCode,
};
}
if (response.statusCode == 200) {
try {
return jsonDecode(response.body);
} catch (e) {
return wrap('failure',
message: 'JSON decode error', code: response.statusCode);
}
} else if (response.statusCode == 401) {
try {
final jsonData = jsonDecode(response.body);
if (jsonData is Map && jsonData['error'] == 'Token expired') {
await Get.put(LoginController()).getJWT();
return {
'status': 'failure',
'message': 'token_expired',
'code': 401
};
}
return wrap('failure', message: jsonData);
} catch (_) {
return wrap('failure', message: response.body);
}
} else {
try {
final jsonData = jsonDecode(response.body);
return wrap('failure', message: jsonData);
} catch (_) {
return wrap('failure', message: response.body);
}
}
} catch (e) {
return {
'status': 'failure',
'message': 'HTTP request error: $e',
'code': -1
};
}
}
Future sendWhatsAppAuth(String to, String token) async {
var res = await CRUD()
.get(link: AppLink.getApiKey, payload: {'keyName': 'whatsapp_key'});
var accesstoken = jsonDecode(res)['message']['whatsapp_key'];
var headers = {
'Authorization': 'Bearer $accesstoken',
'Content-Type': 'application/json'
};
var url = 'https://graph.facebook.com/v20.0/${Env.whatappID}/messages';
var request = http.Request('POST', Uri.parse(url));
var body = json.encode({
"messaging_product": "whatsapp",
"to": to,
"type": "template",
"template": {
"name": "sefer1",
"language": {"code": "en"},
"components": [
{
"type": "body",
"parameters": [
{"type": "text", "text": token}
]
}
]
}
});
request.body = body;
request.headers.addAll(headers);
try {
http.StreamedResponse response = await request.send();
if (response.statusCode == 200) {
String responseBody = await response.stream.bytesToString();
Get.defaultDialog(
title: 'You will receive a code in WhatsApp Messenger'.tr,
middleText: 'wait 1 minute to recive message'.tr,
confirm: MyElevatedButton(
title: 'OK'.tr,
onPressed: () => Get.back(),
),
);
} else {
String errorBody = await response.stream.bytesToString();
}
} catch (e) {
Log.print("Error occurred: $e");
}
}
Future<dynamic> getAgoraToken({
required String channelName,
required String uid,
}) async {
var uid = box.read(BoxName.phone) ?? box.read(BoxName.phoneDriver);
var res = await http.get(
Uri.parse(
'https://orca-app-b2i85.ondigitalocean.app/token?channelName=$channelName'),
headers: {'Authorization': 'Bearer ${AK.agoraAppCertificate}'},
);
if (res.statusCode == 200) {
var response = jsonDecode(res.body);
return response['token'];
}
}
Future<dynamic> getLlama({
required String link,
required String payload,
required String prompt,
}) async {
var url = Uri.parse(link);
var headers = {
'Content-Type': 'application/json',
'Authorization':
'Bearer LL-X5lJ0Px9CzKK0HTuVZ3u2u4v3tGWkImLTG7okGRk4t25zrsLqJ0qNoUzZ2x4ciPy'
};
var data = json.encode({
"model": "Llama-3-70b-Inst-FW",
"messages": [
{
"role": "user",
"content":
"Extract the desired information from the following passage as json decoded like $prompt just in this:\n\n$payload"
}
],
"temperature": 0.9
});
var response = await http.post(url, body: data, headers: headers);
if (response.statusCode == 200) return response.body;
return response.statusCode;
}
Future allMethodForAI(String prompt, linkPHP, imagePath) async {
await ImageController().choosImage(linkPHP, imagePath);
Future.delayed(const Duration(seconds: 2));
String extracted =
await arabicTextExtractByVisionAndAI(imagePath: imagePath);
}
Future<dynamic> arabicTextExtractByVisionAndAI({
required String imagePath,
}) async {
var headers = {
'Content-Type': 'application/json',
'Ocp-Apim-Subscription-Key': '21010e54b50f41a4904708c526e102df'
};
var url = Uri.parse(
'https://ocrhamza.cognitiveservices.azure.com/vision/v2.1/ocr?language=ar');
String imagePathFull =
'${AppLink.server}card_image/$imagePath-${box.read(BoxName.driverID) ?? box.read(BoxName.passengerID)}.jpg';
var requestBody = {"url": imagePathFull};
var response =
await http.post(url, body: jsonEncode(requestBody), headers: headers);
if (response.statusCode == 200) {
var responseBody = jsonDecode(response.body);
return responseBody.toString();
}
return response.statusCode;
}
Future<dynamic> getChatGPT({
required String link,
required String payload,
}) async {
var url = Uri.parse(link);
var headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ${Env.chatGPTkeySeferNew}'
};
var data = json.encode({
"model": "gpt-3.5-turbo",
"messages": [
{
"role": "user",
"content":
"Extract the desired information from the following passage as json decoded like vin,make,made,year,expiration_date,color,owner,registration_date just in this:\n\n$payload"
}
],
"temperature": 0.9
});
var response = await http.post(url, body: data, headers: headers);
if (response.statusCode == 200) return response.body;
return response.statusCode;
}
Future<dynamic> postPayMob({
required String link,
Map<String, dynamic>? payload,
}) async {
var url = Uri.parse(link);
var response = await http.post(url,
body: payload, headers: {'Content-Type': 'application/json'});
var jsonData = jsonDecode(response.body);
if (response.statusCode == 200) {
if (jsonData['status'] == 'success') return response.body;
return jsonData['status'];
} else {
return response.statusCode;
}
}
sendEmail(String link, Map<String, String>? payload) async {
var headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization':
'Basic ${base64Encode(utf8.encode(AK.basicAuthCredentials))}',
};
var request = http.Request('POST', Uri.parse(link));
request.bodyFields = payload!;
request.headers.addAll(headers);
await request.send();
}
Future<dynamic> postFromDialogue({
required String link,
Map<String, dynamic>? payload,
}) async {
var url = Uri.parse(link);
var response = await http.post(
url,
body: payload,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization':
'Basic ${base64Encode(utf8.encode(AK.basicAuthCredentials))}',
},
);
if (response.body.isNotEmpty) {
var jsonData = jsonDecode(response.body);
if (response.statusCode == 200) {
if (jsonData['status'] == 'success') {
Get.back();
return response.body;
}
}
return jsonData['status'];
}
}
Future<void> sendVerificationRequest(String phoneNumber) async {
final accountSid = AK.accountSIDTwillo;
final authToken = AK.authTokenTwillo;
final verifySid = AK.twilloRecoveryCode;
final Uri verificationUri = Uri.parse(
'https://verify.twilio.com/v2/Services/$verifySid/Verifications');
await http.post(
verificationUri,
headers: {
'Authorization':
'Basic ' + base64Encode(utf8.encode('$accountSid:$authToken')),
'Content-Type': 'application/x-www-form-urlencoded',
},
body: {'To': phoneNumber, 'Channel': 'sms'},
);
final otpCode = "123456";
final checkUri = Uri.parse(
'https://verify.twilio.com/v2/Services/$verifySid/VerificationCheck');
final checkResponse = await http.post(
checkUri,
headers: {
'Authorization':
'Basic ' + base64Encode(utf8.encode('$accountSid:$authToken')),
'Content-Type': 'application/x-www-form-urlencoded',
},
body: {'To': phoneNumber, 'Code': otpCode},
);
}
Future<dynamic> getGoogleApi({
required String link,
Map<String, dynamic>? payload,
}) async {
var url = Uri.parse(link);
var response = await http.post(url, body: payload);
var jsonData = jsonDecode(response.body);
if (jsonData['status'] == 'OK') return jsonData;
return jsonData['status'];
}
Future<dynamic> getHereMap({required String link}) async {
var url = Uri.parse(link);
try {
var response = await http.get(url);
if (response.statusCode == 200) {
var decodedBody = utf8.decode(response.bodyBytes);
return jsonDecode(decodedBody);
}
return null;
} catch (e) {
return null;
}
}
Future<dynamic> getMapSaas({
required String link,
}) async {
var url = Uri.parse(link);
try {
var response = await http.get(
url,
headers: {
'Content-Type': 'application/json',
'x-api-key': Env.mapSaasKey,
},
);
Log.print('link -MapSaas: $link');
Log.print('response -MapSaas: ${response.body}');
if (response.statusCode == 200) {
return jsonDecode(response.body);
}
Log.print('MapSaas Error: ${response.statusCode} - ${response.body}');
return null;
} catch (e) {
Log.print('MapSaas Exception: $e');
return null;
}
}
Future<dynamic> postMapSaas({
required String link,
required Map<String, dynamic> payload,
}) async {
var url = Uri.parse(link);
try {
var response = await http.post(
url,
body: jsonEncode(payload),
headers: {
'Content-Type': 'application/json',
'x-api-key': Env.mapSaasKey,
},
);
Log.print('post -MapSaas link: $link');
Log.print('post -MapSaas payload: $payload');
Log.print('post -MapSaas response: ${response.body}');
if (response.statusCode == 200 || response.statusCode == 201) {
return jsonDecode(response.body);
}
Log.print(
'MapSaas Post Error: ${response.statusCode} - ${response.body}');
return null;
} catch (e) {
Log.print('MapSaas Post Exception: $e');
return null;
}
}
}

View File

@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
class LineChartPainter extends CustomPainter {
final List<double> data;
LineChartPainter(this.data);
@override
void paint(Canvas canvas, Size size) {
// Calculate the scale factor.
final scaleFactor = size.height / 240;
// Draw the line chart.
for (var i = 0; i < data.length - 1; i++) {
final x1 = i * size.width / data.length;
final y1 = data[i] * scaleFactor;
final x2 = (i + 1) * size.width / data.length;
final y2 = data[i + 1] * scaleFactor;
canvas.drawLine(Offset(x1, y1), Offset(x2, y2), Paint());
}
}
@override
bool shouldRepaint(LineChartPainter oldDelegate) => false;
}

View File

@@ -0,0 +1,92 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import '../../print.dart';
class DeviceInfoPlus {
static List<Map<String, dynamic>> deviceDataList = [];
static Future<List<Map<String, dynamic>>> getDeviceInfo() async {
final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
try {
if (Platform.isAndroid) {
AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo;
Map<String, dynamic> deviceData = {
'platform': 'Android',
'brand': androidInfo.brand,
'model': androidInfo.model,
'androidId': androidInfo.device,
'versionRelease': androidInfo.version.release,
'sdkVersion': androidInfo.version.sdkInt,
'manufacturer': androidInfo.manufacturer,
'isPhysicalDevice': androidInfo.isPhysicalDevice,
'serialNumber': androidInfo.fingerprint,
'fingerprint': androidInfo.fingerprint,
'type': androidInfo.type,
'data': androidInfo.data,
'version': androidInfo.version,
'tags': androidInfo.tags,
'display': androidInfo.display,
};
// Log.print('deviceData: ${deviceData}');
deviceDataList.add(deviceData);
} else if (Platform.isIOS) {
IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo;
Map<String, dynamic> deviceData = {
'brand': 'Apple',
'model': iosInfo.model,
'systemName': iosInfo.systemName,
'systemVersion': iosInfo.systemVersion,
'utsname': iosInfo.utsname,
'isPhysicalDevice': iosInfo.isPhysicalDevice,
'identifierForVendor': iosInfo.identifierForVendor,
'name': iosInfo.name,
'localizedModel': iosInfo.localizedModel,
};
deviceDataList.add(deviceData);
} else if (Platform.isMacOS) {
MacOsDeviceInfo macInfo = await deviceInfoPlugin.macOsInfo;
Map<String, dynamic> deviceData = {
'platform': 'macOS',
'model': macInfo.model,
'version': macInfo.systemGUID,
};
deviceDataList.add(deviceData);
} else if (Platform.isWindows) {
WindowsDeviceInfo windowsInfo = await deviceInfoPlugin.windowsInfo;
Map<String, dynamic> deviceData = {
'platform': 'Windows',
'manufacturer': windowsInfo.computerName,
'version': windowsInfo.majorVersion,
'deviceId': windowsInfo.deviceId,
'userName': windowsInfo.userName,
'productName': windowsInfo.productName,
'installDate': windowsInfo.installDate,
'productId': windowsInfo.productId,
'numberOfCores': windowsInfo.numberOfCores,
'systemMemoryInMegabytes': windowsInfo.systemMemoryInMegabytes,
};
deviceDataList.add(deviceData);
} else if (Platform.isLinux) {
LinuxDeviceInfo linuxInfo = await deviceInfoPlugin.linuxInfo;
Map<String, dynamic> deviceData = {
'platform': 'Linux',
'manufacturer': linuxInfo.name,
'version': linuxInfo.version,
};
deviceDataList.add(deviceData);
}
} catch (e) { Log.print("Error occurred: $e"); }
return deviceDataList;
}
// Method to print all device data
static void printDeviceInfo() {
for (Map<String, dynamic> deviceData in deviceDataList) {
'Version: ${deviceData['version'] ?? deviceData['versionRelease'] ?? 'N/A'}';
}
}
}

View File

@@ -0,0 +1,42 @@
// import 'package:flutter/services.dart';
// class DigitObscuringFormatter extends TextInputFormatter {
// @override
// TextEditingValue formatEditUpdate(
// TextEditingValue oldValue, TextEditingValue newValue) {
// final maskedText = maskDigits(newValue.text);
// return newValue.copyWith(
// text: maskedText,
// selection: updateCursorPosition(maskedText, newValue.selection));
// }
// String maskDigits(String text) {
// final totalDigits = text.length;
// final visibleDigits = 4;
// final hiddenDigits = totalDigits - visibleDigits * 2;
// final firstVisibleDigits = text.substring(0, visibleDigits);
// final lastVisibleDigits = text.substring(totalDigits - visibleDigits);
// final maskedDigits = List.filled(hiddenDigits, '*').join();
// return '$firstVisibleDigits$maskedDigits$lastVisibleDigits';
// }
// TextSelection updateCursorPosition(
// String maskedText, TextSelection currentSelection) {
// final cursorPosition = currentSelection.baseOffset;
// final cursorOffset =
// currentSelection.extentOffset - currentSelection.baseOffset;
// final totalDigits = maskedText.length;
// const visibleDigits = 4;
// final hiddenDigits = totalDigits - visibleDigits * 2;
// final updatedPosition = cursorPosition <= visibleDigits
// ? cursorPosition
// : hiddenDigits + visibleDigits + (cursorPosition - visibleDigits);
// return TextSelection.collapsed(
// offset: updatedPosition, affinity: currentSelection.affinity);
// }
// }

View File

@@ -0,0 +1,75 @@
import 'package:siro_rider/env/env.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:flutter/foundation.dart';
import 'package:secure_string_operations/secure_string_operations.dart';
import '../../constant/char_map.dart';
class EncryptionHelper {
static EncryptionHelper? _instance;
late final encrypt.Key key;
late final encrypt.IV iv;
EncryptionHelper._(this.key, this.iv);
static EncryptionHelper get instance {
if (_instance == null) {
throw Exception(
"EncryptionHelper is not initialized. Call `await EncryptionHelper.initialize()` in main.");
}
return _instance!;
}
/// Initializes and stores the instance globally
static Future<void> initialize() async {
if (_instance != null) {
debugPrint("EncryptionHelper is already initialized.");
return; // Prevent re-initialization
}
debugPrint("Initializing EncryptionHelper...");
var keyOfApp = r(Env.keyOfApp).toString().split(Env.addd)[0];
var initializationVector =
r(Env.initializationVector).toString().split(Env.addd)[0];
// Set the global instance
_instance = EncryptionHelper._(
encrypt.Key.fromUtf8(keyOfApp!),
encrypt.IV.fromUtf8(initializationVector!),
);
debugPrint("EncryptionHelper initialized successfully.");
}
/// Encrypts a string
String encryptData(String plainText) {
try {
final encrypter =
encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc));
final encrypted = encrypter.encrypt(plainText, iv: iv);
return encrypted.base64;
} catch (e) {
debugPrint('Encryption Error: $e');
return '';
}
}
/// Decrypts a string
String decryptData(String encryptedText) {
try {
final encrypter =
encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc));
final encrypted = encrypt.Encrypted.fromBase64(encryptedText);
return encrypter.decrypt(encrypted, iv: iv);
} catch (e) {
debugPrint('Decryption Error: $e');
return '';
}
}
}
r(String string) {
return X.r(X.r(X.r(string, cn), cC), cs).toString();
}
c(String string) {
return X.c(X.c(X.c(string, cn), cC), cs).toString();
}

View File

@@ -0,0 +1,34 @@
import 'package:geolocator/geolocator.dart';
class GeoLocation {
Future<Position> getCurrentLocation() async {
bool serviceEnabled;
LocationPermission permission;
// Check if location services are enabled.
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
// Location services are not enabled, so we request the user to enable it.
return Future.error('Location services are disabled.');
}
permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
// Permissions are denied, we cannot fetch the location.
return Future.error('Location permissions are denied');
}
}
if (permission == LocationPermission.deniedForever) {
// Permissions are denied forever, we cannot request permissions.
return Future.error(
'Location permissions are permanently denied, we cannot request permissions.');
}
// When we reach here, permissions are granted and we can fetch the location.
return await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
}
}

View File

@@ -0,0 +1,108 @@
import 'package:siro_rider/print.dart';
import 'package:url_launcher/url_launcher.dart';
import 'dart:io';
void showInBrowser(String url) async {
if (await canLaunchUrl(Uri.parse(url))) {
launchUrl(Uri.parse(url));
} else {}
}
Future<void> makePhoneCall(String phoneNumber) async {
// 1. تنظيف الرقم (إزالة المسافات والفواصل)
String formattedNumber = phoneNumber.replaceAll(RegExp(r'\s+'), '');
// 2. منطق التنسيق (مع الحفاظ على الأرقام القصيرة مثل 112 كما هي)
if (formattedNumber.length > 6) {
if (formattedNumber.startsWith('09')) {
// إذا كان يبدأ بـ 09 (رقم موبايل سوري محلي) -> +963
formattedNumber = '+963${formattedNumber.substring(1)}';
} else if (!formattedNumber.startsWith('+')) {
// إذا لم يكن دولياً ولا محلياً معروفاً -> إضافة + فقط
formattedNumber = '+$formattedNumber';
}
}
// ملاحظة: الأرقام القصيرة (مثل 112) ستتجاوز الشرط أعلاه وتبقى "112" وهو الصحيح
// 3. التنفيذ (Launch)
final Uri launchUri = Uri(
scheme: 'tel',
path: formattedNumber,
);
try {
// استخدام LaunchMode.externalApplication هو الحل الجذري لمشاكل الـ Intent
// لأنه يجبر النظام على تسليم الرابط لتطبيق الهاتف بدلاً من محاولة فتحه داخل تطبيقك
if (await canLaunchUrl(launchUri)) {
await launchUrl(launchUri, mode: LaunchMode.externalApplication);
} else {
// في بعض الأجهزة canLaunchUrl تعود بـ false مع الـ tel ومع ذلك يعمل launchUrl
// لذا نجرب الإطلاق المباشر كاحتياط
await launchUrl(launchUri, mode: LaunchMode.externalApplication);
}
} catch (e) {
// طباعة الخطأ في حال الفشل التام
Log.print("Error launching call: $e");
}
}
void launchCommunication(
String method, String contactInfo, String message) async {
String url;
if (Platform.isIOS) {
switch (method) {
case 'phone':
url = 'tel:$contactInfo';
break;
case 'sms':
url = 'sms:$contactInfo?body=${Uri.encodeComponent(message)}';
break;
case 'whatsapp':
url =
'https://api.whatsapp.com/send?phone=$contactInfo&text=${Uri.encodeComponent(message)}';
break;
case 'email':
url =
'mailto:$contactInfo?subject=Subject&body=${Uri.encodeComponent(message)}';
break;
default:
return;
}
} else if (Platform.isAndroid) {
switch (method) {
case 'phone':
url = 'tel:$contactInfo';
break;
case 'sms':
url = 'sms:$contactInfo?body=${Uri.encodeComponent(message)}';
break;
case 'whatsapp':
// Check if WhatsApp is installed
final bool whatsappInstalled =
await canLaunchUrl(Uri.parse('whatsapp://'));
if (whatsappInstalled) {
url =
'whatsapp://send?phone=$contactInfo&text=${Uri.encodeComponent(message)}';
} else {
// Provide an alternative action, such as opening the WhatsApp Web API
url =
'https://api.whatsapp.com/send?phone=$contactInfo&text=${Uri.encodeComponent(message)}';
}
break;
case 'email':
url =
'mailto:$contactInfo?subject=Subject&body=${Uri.encodeComponent(message)}';
break;
default:
return;
}
} else {
return;
}
if (await canLaunchUrl(Uri.parse(url))) {
await launchUrl(Uri.parse(url));
} else {}
}

View File

@@ -0,0 +1,147 @@
// import 'dart:async';
// import 'package:get/get.dart';
// import 'package:location/location.dart';
// import 'package:siro_rider/constant/box_name.dart';
// import 'package:siro_rider/constant/links.dart';
// import 'package:siro_rider/controller/functions/crud.dart';
// import 'package:siro_rider/controller/home/payment/captain_wallet_controller.dart';
// import 'package:siro_rider/main.dart';
// // LocationController.dart
// class LocationController extends GetxController {
// LocationData? _currentLocation;
// late Location location;
// bool isLoading = false;
// late double heading = 0;
// late double accuracy = 0;
// late double previousTime = 0;
// late double latitude;
// late double totalDistance = 0;
// late double longitude;
// late DateTime time;
// late double speed = 0;
// late double speedAccuracy = 0;
// late double headingAccuracy = 0;
// bool isActive = false;
// late LatLng myLocation;
// String totalPoints = '0';
// LocationData? get currentLocation => _currentLocation;
// Timer? _locationTimer;
// @override
// void onInit() async {
// super.onInit();
// location = Location();
// getLocation();
// // startLocationUpdates();
// totalPoints = Get.put(CaptainWalletController()).totalPoints;
// }
// Future<void> startLocationUpdates() async {
// if (box.read(BoxName.driverID) != null) {
// _locationTimer =
// Timer.periodic(const Duration(seconds: 5), (timer) async {
// try {
// totalPoints = Get.find<CaptainWalletController>().totalPoints;
// // if (isActive) {
// if (double.parse(totalPoints) > -300) {
// await getLocation();
// // if (box.read(BoxName.driverID) != null) {
// await CRUD()
// .post(link: AppLink.addCarsLocationByPassenger, payload: {
// 'driver_id': box.read(BoxName.driverID).toString(),
// 'latitude': myLocation.latitude.toString(),
// 'longitude': myLocation.longitude.toString(),
// 'heading': heading.toString(),
// 'speed': (speed * 3.6).toStringAsFixed(1),
// 'distance': totalDistance == 0
// ? '0'
// : totalDistance < 1
// ? totalDistance.toStringAsFixed(3)
// : totalDistance.toStringAsFixed(1),
// 'status': box.read(BoxName.statusDriverLocation).toString()
// });
// }
// // }
// } catch (e) {
// // Handle the error gracefully
// }
// });
// }
// }
// void stopLocationUpdates() {
// _locationTimer?.cancel();
// }
// Future<void> getLocation() async {
// // isLoading = true;
// // update();
// bool serviceEnabled;
// PermissionStatus permissionGranted;
// // Check if location services are enabled
// serviceEnabled = await location.serviceEnabled();
// if (!serviceEnabled) {
// serviceEnabled = await location.requestService();
// if (!serviceEnabled) {
// // Location services are still not enabled, handle the error
// return;
// }
// }
// // Check if the app has permission to access location
// permissionGranted = await location.hasPermission();
// if (permissionGranted == PermissionStatus.denied) {
// permissionGranted = await location.requestPermission();
// if (permissionGranted != PermissionStatus.granted) {
// // Location permission is still not granted, handle the error
// return;
// }
// }
// // Configure location accuracy
// // LocationAccuracy desiredAccuracy = LocationAccuracy.high;
// // Get the current location
// LocationData _locationData = await location.getLocation();
// myLocation =
// (_locationData.latitude != null && _locationData.longitude != null
// ? LatLng(_locationData.latitude!, _locationData.longitude!)
// : null)!;
// speed = _locationData.speed!;
// heading = _locationData.heading!;
// // Calculate the distance between the current location and the previous location
// // if (Get.find<HomeCaptainController>().rideId == 'rideId') {
// // if (previousTime > 0) {
// // double distance = calculateDistanceInKmPerHour(
// // previousTime, _locationData.time, speed);
// // totalDistance += distance;
// // }
// // previousTime = _locationData.time!;
// // }
// // Print location details
// // isLoading = false;
// update();
// }
// double calculateDistanceInKmPerHour(
// double? startTime, double? endTime, double speedInMetersPerSecond) {
// // Calculate the time difference in hours
// double timeDifferenceInHours = (endTime! - startTime!) / 1000 / 3600;
// // Convert speed to kilometers per hour
// double speedInKmPerHour = speedInMetersPerSecond * 3.6;
// // Calculate the distance in kilometers
// double distanceInKilometers = speedInKmPerHour * timeDifferenceInHours;
// return distanceInKilometers;
// }
// }

View File

@@ -0,0 +1,16 @@
import 'package:location/location.dart';
import 'package:get/get.dart';
class LocationPermissions {
late Location location;
Future locationPermissions() async {
location = Location();
var permissionStatus = await location.requestPermission();
if (permissionStatus == PermissionStatus.denied) {
// The user denied the location permission.
Get.defaultDialog(title: 'GPS Required Allow !.'.tr, middleText: '');
return null;
}
}
}

View File

@@ -0,0 +1,201 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/constant/colors.dart';
import 'package:siro_rider/constant/links.dart';
import 'package:siro_rider/controller/functions/crud.dart';
import 'package:siro_rider/main.dart';
import 'package:siro_rider/onbording_page.dart';
import 'package:siro_rider/views/widgets/elevated_btn.dart';
import 'package:siro_rider/views/widgets/my_textField.dart';
import '../../constant/style.dart';
import 'package:siro_rider/controller/home/map/map_socket_controller.dart';
import 'package:siro_rider/controller/home/map/map_engine_controller.dart';
import 'package:siro_rider/controller/home/map/location_search_controller.dart';
import 'package:siro_rider/controller/home/map/nearby_drivers_controller.dart';
import 'package:siro_rider/controller/home/map/ride_lifecycle_controller.dart';
import 'package:siro_rider/controller/home/map/ui_interactions_controller.dart';
import 'package:siro_rider/controller/home/menu_controller.dart';
import 'package:siro_rider/controller/home/points_for_rider_controller.dart';
class LogOutController extends GetxController {
TextEditingController checkTxtController = TextEditingController();
final formKey = GlobalKey<FormState>();
final formKey1 = GlobalKey<FormState>();
final emailTextController = TextEditingController();
Future deleteMyAccountDriver(String id) async {
await CRUD().post(link: AppLink.removeUser, payload: {'id': id}).then(
(value) => Get.snackbar('Deleted'.tr, 'Your Account is Deleted',
backgroundColor: AppColor.redColor));
}
checkBeforeDelete() async {
var res = await CRUD().post(
link: AppLink.deletecaptainAccounr,
payload: {'id': box.read(BoxName.driverID)}).then((value) => exit(0));
}
deletecaptainAccount() {
Get.defaultDialog(
backgroundColor: AppColor.yellowColor,
title: 'Are you sure to delete your account?'.tr,
middleText:
'Your data will be erased after 2 weeks\nAnd you will can\'t return to use app after 1 month ',
titleStyle: AppStyle.title,
content: Column(
children: [
Container(
width: Get.width,
decoration: AppStyle.boxDecoration,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Your data will be erased after 2 weeks\nAnd you will can\'t return to use app after 1 month'
.tr,
style: AppStyle.title.copyWith(color: AppColor.redColor),
),
),
),
const SizedBox(
height: 20,
),
Form(
key: formKey,
child: SizedBox(
width: Get.width,
child: MyTextForm(
controller: checkTxtController,
label: 'Enter Your First Name'.tr,
hint: 'Enter Your First Name'.tr,
type: TextInputType.name,
),
))
],
),
confirm: MyElevatedButton(
title: 'Delete'.tr,
onPressed: () {
if (checkTxtController.text == box.read(BoxName.nameDriver)) {
deletecaptainAccount();
}
}));
}
Future logOutPassenger() async {
Get.defaultDialog(
title: 'Are you Sure to LogOut?'.tr,
content: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
MyElevatedButton(
title: 'Cancel'.tr,
onPressed: () => Get.back(),
),
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(AppColor.redColor),
),
onPressed: () {
// box.remove(BoxName.agreeTerms);
box.remove(BoxName.passengerPhotoUrl);
box.remove(BoxName.driverID);
box.remove(BoxName.email);
box.remove(BoxName.lang);
box.remove(BoxName.name);
box.remove(BoxName.passengerID);
box.remove(BoxName.phone);
box.remove(BoxName.tokenFCM);
box.remove(BoxName.tokens);
box.remove(BoxName.addHome);
box.remove(BoxName.addWork);
box.remove(BoxName.agreeTerms);
box.remove(BoxName.apiKeyRun);
box.remove(BoxName.countryCode);
box.remove(BoxName.passengerWalletTotal);
box.remove(BoxName.isVerified);
Get.delete<MapSocketController>(force: true);
Get.delete<MapEngineController>(force: true);
Get.delete<LocationSearchController>(force: true);
Get.delete<NearbyDriversController>(force: true);
Get.delete<RideLifecycleController>(force: true);
Get.delete<UiInteractionsController>(force: true);
Get.delete<MyMenuController>(force: true);
Get.delete<CRUD>(force: true);
Get.delete<WayPointController>(force: true);
Get.offAll(OnBoardingPage());
},
child: Text(
'Sign Out'.tr,
style:
AppStyle.title.copyWith(color: AppColor.secondaryColor),
))
],
));
}
Future logOutCaptain() async {
Get.defaultDialog(
title: 'Are you Sure to LogOut?'.tr,
titleStyle: AppStyle.title,
content: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
MyElevatedButton(
title: 'Cancel'.tr,
onPressed: () => Get.back(),
),
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(AppColor.redColor),
),
onPressed: () {
// box.remove(BoxName.agreeTerms);
box.remove(BoxName.driverID);
box.remove(BoxName.sexDriver);
box.remove(BoxName.dobDriver);
box.remove(BoxName.nameDriver);
box.remove(BoxName.emailDriver);
box.remove(BoxName.phoneDriver);
box.remove(BoxName.statusDriverLocation);
box.remove(BoxName.cvvCodeDriver);
box.remove(BoxName.lastNameDriver);
box.remove(BoxName.passwordDriver);
box.remove(BoxName.cardNumberDriver);
box.remove(BoxName.expiryDateDriver);
box.remove(BoxName.cardHolderNameDriver);
box.remove(BoxName.vin);
box.remove(BoxName.make);
box.remove(BoxName.year);
box.remove(BoxName.owner);
box.remove(BoxName.onBoarding);
box.remove(BoxName.agreeTerms);
Get.offAll(OnBoardingPage());
},
child: Text(
'Sign Out'.tr,
style:
AppStyle.title.copyWith(color: AppColor.secondaryColor),
))
],
));
}
deletePassengerAccount() async {
if (formKey1.currentState!.validate()) {
if (box.read(BoxName.email).toString() == emailTextController.text) {
await CRUD().post(link: AppLink.passengerRemovedAccountEmail, payload: {
'email': box.read(BoxName.email),
});
} else {
Get.snackbar('Email Wrong'.tr, 'Email you inserted is Wrong.'.tr,
snackPosition: SnackPosition.BOTTOM,
backgroundColor: AppColor.redColor);
}
}
}
}

View File

@@ -0,0 +1,48 @@
import 'dart:async';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'net_guard.dart';
typedef BodyEncoder = Future<http.Response> Function();
class HttpRetry {
/// ريتراي لـ network/transient errors فقط.
static Future<http.Response> sendWithRetry(
BodyEncoder send, {
int maxRetries = 3,
Duration baseDelay = const Duration(milliseconds: 400),
Duration timeout = const Duration(seconds: 12),
}) async {
// ✅ Pre-flight check for internet connection
if (!await NetGuard().hasInternet()) {
// Immediately throw a specific exception if there's no internet.
// This avoids pointless retries.
throw const SocketException("No internet connection");
}
int attempt = 0;
while (true) {
attempt++;
try {
final res = await send().timeout(timeout);
return res;
} on TimeoutException catch (_) {
if (attempt >= maxRetries) rethrow;
} on SocketException catch (_) {
if (attempt >= maxRetries) rethrow;
} on HandshakeException catch (_) {
if (attempt >= maxRetries) rethrow;
} on http.ClientException catch (e) {
// مثال: Connection reset by peer
final msg = e.message.toLowerCase();
final transient = msg.contains('connection reset') ||
msg.contains('broken pipe') ||
msg.contains('timed out');
if (!transient || attempt >= maxRetries) rethrow;
}
// backoff: 0.4s, 0.8s, 1.6s
final delay = baseDelay * (1 << (attempt - 1));
await Future.delayed(delay);
}
}
}

View File

@@ -0,0 +1,48 @@
import 'dart:async';
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:internet_connection_checker/internet_connection_checker.dart';
class NetGuard {
static final NetGuard _i = NetGuard._();
NetGuard._();
factory NetGuard() => _i;
bool _notified = false;
/// فحص: (أ) فيه شبكة؟ (ب) فيه انترنت؟ (ج) السيرفر نفسه reachable؟
Future<bool> hasInternet({Uri? mustReach}) async {
final connectivity = await Connectivity().checkConnectivity();
if (connectivity == ConnectivityResult.none) return false;
final hasNet =
await InternetConnectionChecker.createInstance().hasConnection;
if (!hasNet) return false;
if (mustReach != null) {
try {
final host = mustReach.host;
final result = await InternetAddress.lookup(host);
if (result.isEmpty || result.first.rawAddress.isEmpty) return false;
// اختباري خفيف عبر TCP (80/443) — 400ms timeout
final port = mustReach.scheme == 'http' ? 80 : 443;
final socket = await Socket.connect(host, port,
timeout: const Duration(milliseconds: 400));
socket.destroy();
} catch (_) {
return false;
}
}
return true;
}
/// إظهار إشعار مرة واحدة ثم إسكات التكرارات
void notifyOnce(void Function(String title, String msg) show) {
if (_notified) return;
_notified = true;
show('لا يوجد اتصال بالإنترنت', 'تحقق من الشبكة ثم حاول مجددًا.');
// إعادة السماح بعد 15 ثانية
Future.delayed(const Duration(seconds: 15), () => _notified = false);
}
}

View File

@@ -0,0 +1,358 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:ui';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:jailbreak_root_detection/jailbreak_root_detection.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../constant/box_name.dart';
import '../../constant/colors.dart';
import '../../main.dart';
import '../../print.dart';
import 'encrypt_decrypt.dart';
Future<void> checkForUpdate(BuildContext context) async {
final packageInfo = await PackageInfo.fromPlatform();
final currentVersion = packageInfo.buildNumber;
final version = packageInfo.version;
Log.print('currentVersion is : $currentVersion');
// Fetch the latest version from your server
String latestVersion = box.read(BoxName.package);
box.write(BoxName.packagInfo, version);
if (latestVersion.isNotEmpty && latestVersion != currentVersion) {
showUpdateDialog(context);
}
}
checkForBounusInvitation() {
if (box.read(BoxName.inviteCode) != null) {}
}
// Future<String> getPackageInfo() async {
// final response = await CRUD().get(link: AppLink.packageInfo, payload: {
// "platform": Platform.isAndroid ? 'android' : 'ios',
// "appName": AppInformation.appName,
// });
// if (response != 'failure') {
// return jsonDecode(response)['message'][0]['version'];
// }
// return '';
// }
// getDeviceFingerprint() async {
// final deviceInfo = await DeviceInfoPlugin().deviceInfo;
// var deviceData;
// if (Platform.isAndroid) {
// deviceData = deviceInfo.data;
// Log.print('deviceData: ${jsonEncode(deviceData)}');
// } else if (Platform.isIOS) {
// deviceData = deviceInfo.data;
// }
// final String deviceId =
// deviceData['device'] ?? deviceData['identifierForVendor'];
// final String deviceModel = deviceData['model'];
// final String osVersion = deviceData['systemVersion'];
// Log.print('fingr: ${'${deviceId}_${deviceModel}_$osVersion'}');
// Log.print('deviceModel: ${deviceModel}');
// Log.print('osVersion: ${osVersion}');
// return EncryptionHelper.instance
// .encryptData('${deviceId}_${deviceModel}_$osVersion');
// }
void showUpdateDialog(BuildContext context) {
final String storeUrl = Platform.isAndroid
? 'https://play.google.com/store/apps/details?id=com.Intaleq.intaleq'
: 'https://apps.apple.com/jo/app/intaleq-rider/id6748075179';
showGeneralDialog(
context: context,
barrierDismissible: false,
barrierColor: Colors.black.withOpacity(0.5),
pageBuilder: (_, __, ___) {
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Center(
child: AlertDialog(
// Using AlertDialog for a more Material Design look
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(16)), // More rounded corners
elevation: 4, // Add a bit more elevation
contentPadding: EdgeInsets.zero, // Remove default content padding
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(top: 20.0),
child: Image.asset(
'assets/images/logo.png',
height: 72, // Slightly larger logo
width: 72,
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Text(
'Update Available'.tr,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
// Use theme's title style
fontWeight: FontWeight.bold,
),
),
),
Padding(
padding: const EdgeInsets.all(24.0),
child: Text(
'A new version of the app is available. Please update to the latest version.'
.tr, // More encouraging message
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
// Use theme's body style
color: Colors.black87,
),
),
),
const Divider(height: 0),
Row(
children: [
Expanded(
child: TextButton(
// Using TextButton for "Cancel"
onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom(
foregroundColor: Colors.grey,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(16),
),
),
),
child: Text('Cancel'.tr),
),
),
const SizedBox(
height: 48,
child: VerticalDivider(width: 0), // Using VerticalDivider
),
Expanded(
child: ElevatedButton(
// Using ElevatedButton for "Update"
onPressed: () async {
if (await canLaunchUrl(Uri.parse(storeUrl))) {
await launchUrl(Uri.parse(storeUrl));
}
if (context.mounted) Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColor
.primaryColor, // Use theme's primary color
foregroundColor: Theme.of(context)
.colorScheme
.onPrimary, // Use theme's onPrimary color
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomRight: Radius.circular(16),
),
),
),
child: Text('Update'.tr),
),
),
],
),
],
),
),
),
);
},
transitionBuilder: (_, animation, __, child) {
return ScaleTransition(
scale: CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic, // More natural curve
),
child: child,
);
},
);
}
class SecurityHelper {
/// Performs security checks and handles potential risks
static Future<void> performSecurityChecks() async {
bool isNotTrust = false;
bool isJailBroken = false;
bool isRealDevice = true;
bool isOnExternalStorage = false;
bool checkForIssues = false;
bool isDevMode = false;
bool isTampered = false;
String bundleId = "";
try {
isNotTrust = await JailbreakRootDetection.instance.isNotTrust;
isJailBroken = await JailbreakRootDetection.instance.isJailBroken;
isRealDevice = await JailbreakRootDetection.instance.isRealDevice;
isOnExternalStorage =
await JailbreakRootDetection.instance.isOnExternalStorage;
List<JailbreakIssue> issues =
await JailbreakRootDetection.instance.checkForIssues;
checkForIssues = issues.isNotEmpty;
isDevMode = await JailbreakRootDetection.instance.isDevMode;
// Get Bundle ID
PackageInfo packageInfo = await PackageInfo.fromPlatform();
bundleId = packageInfo.packageName;
if (bundleId.isNotEmpty) {
// Pass the CORRECT bundle ID to isTampered
isTampered = await JailbreakRootDetection.instance.isTampered(bundleId);
}
} catch (e) {
debugPrint("Error during security checks: $e");
// Consider handling specific exceptions, not just general errors.
}
// Save values to storage (using GetStorage)
await box.write('isNotTrust', isNotTrust); // Use await for write operations
await box.write('isTampered', isTampered); // Use await
await box.write('isJailBroken', isJailBroken); // Use await
debugPrint("Security Check Results:");
debugPrint("isNotTrust: $isNotTrust");
debugPrint("isJailBroken: $isJailBroken");
debugPrint("isRealDevice: $isRealDevice");
debugPrint("isOnExternalStorage: $isOnExternalStorage");
debugPrint("checkForIssues: $checkForIssues");
debugPrint("isDevMode: $isDevMode");
debugPrint("isTampered: $isTampered");
debugPrint("Bundle ID: $bundleId"); //Log.print the bundle ID
// Check for security risks and potentially show a warning
if (isJailBroken || isRealDevice == false || isTampered) {
// Log.print("security_warning".tr); //using easy_localization
// Use a more robust approach to show a warning, like a dialog:
_showSecurityWarning();
} else {
box.write(BoxName.security_check, 'passed');
Log.print('Security checks passed successfully.');
}
}
/// Deletes all app data
static void _showSecurityWarning() {
// Use an RxInt to track the remaining seconds. This is the KEY!
RxInt secondsRemaining = 10.obs;
Get.dialog(
CupertinoAlertDialog(
title: Text("Security Warning".tr),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Obx(() => Text(
"Potential security risks detected. The application will close in @seconds seconds."
.trParams({
// Use trParams for placeholders
'seconds': secondsRemaining.value.toString(),
}),
// Wrap the Text widget in Obx
)),
SizedBox(height: 24), // More spacing before the progress bar
Obx(() => SizedBox(
width: double.infinity, // Make progress bar full width
child: CupertinoActivityIndicator(
// in case of loading
radius: 15,
animating: true,
))),
SizedBox(height: 8),
Obx(() => ClipRRect(
borderRadius: BorderRadius.circular(8), // Rounded corners
child: LinearProgressIndicator(
value: secondsRemaining.value / 10,
backgroundColor: Colors.grey.shade300, // Lighter background
valueColor: AlwaysStoppedAnimation<Color>(
CupertinoColors.systemRed), // iOS-style red
minHeight: 8, // Slightly thicker progress bar
),
)),
],
),
),
barrierDismissible: false,
);
Timer.periodic(Duration(seconds: 1), (timer) {
secondsRemaining.value--;
if (secondsRemaining.value <= 0) {
timer.cancel();
// Get.back();
_clearDataAndExit();
}
});
}
static Future<void> _clearDataAndExit() async {
await storage.deleteAll();
await box.erase();
exit(0); // Exit the app
Log.print('exit');
}
}
class DeviceHelper {
static Future<String> getDeviceFingerprint() async {
final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
var deviceData;
try {
if (Platform.isAndroid) {
// Fetch Android-specific device information
AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo;
deviceData = androidInfo.toMap(); // Convert to a map for easier access
// Log.print('deviceData: ${jsonEncode(deviceData)}');
} else if (Platform.isIOS) {
// Fetch iOS-specific device information
IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo;
deviceData = iosInfo.toMap(); // Convert to a map for easier access
} else {
throw UnsupportedError('Unsupported platform');
}
// Extract relevant device information
final String deviceId = Platform.isAndroid
? deviceData['androidId'] ?? deviceData['fingerprint'] ?? 'unknown'
: deviceData['identifierForVendor'] ?? 'unknown';
final String deviceModel = deviceData['model'] ?? 'unknown';
// final String osVersion = Platform.isAndroid
// ? deviceData['version']['release'] ?? 'unknown'
// : deviceData['systemVersion'] ?? 'unknown';
// Log the extracted information
// Generate and return the encrypted fingerprint
final String fingerprint = '${deviceId}_$deviceModel';
final String encryptedFp =
EncryptionHelper.instance.encryptData(fingerprint);
box.write(BoxName.deviceFpEncrypted, encryptedFp);
//Log.print(EncryptionHelper.instance.encryptData(fingerprint));
return encryptedFp;
} catch (e) {
throw Exception('Failed to generate device fingerprint');
}
}
}

View File

@@ -0,0 +1,8 @@
// import 'package:ride/controller/functions/crud.dart';
// class RemoveAccount {
// void removeAccount()async{
// var res=await CRUD().post(link: link)
// }
// }

View File

@@ -0,0 +1,25 @@
// import 'package:credit_card_scanner/credit_card_scanner.dart';
// import 'package:get/get.dart';
//
// class ScanIdCard extends GetxController {
// CardDetails? _cardDetails;
// CardScanOptions scanOptions = const CardScanOptions(
// scanCardHolderName: true,
// enableDebugLogs: true,
// validCardsToScanBeforeFinishingScan: 5,
// possibleCardHolderNamePositions: [
// CardHolderNameScanPosition.aboveCardNumber,
// ],
// );
//
// Future<void> scanCard() async {
// final CardDetails? cardDetails =
// await CardScanner.scanCard(scanOptions: scanOptions);
// if (cardDetails == null) {
// return;
// }
//
// _cardDetails = cardDetails;
// update();
// }
// }

View File

@@ -0,0 +1,84 @@
import 'dart:convert';
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/constant/info.dart';
import 'package:siro_rider/controller/auth/login_controller.dart';
import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:secure_string_operations/secure_string_operations.dart';
import '../../constant/char_map.dart';
import '../../constant/links.dart';
import '../../main.dart';
import '../../print.dart';
import 'crud.dart';
import 'encrypt_decrypt.dart';
class SecureStorage {
void saveData(String key, value) async {
await storage.write(key: key, value: value);
}
Future<String?> readData(String boxName) async {
final String? value = await storage.read(key: boxName);
return value.toString();
}
}
const List<String> keysToFetch = [
'serverPHP',
// 'seferAlexandriaServer',
// 'seferPaymentServer',
// 'seferCairoServer',
// 'seferGizaServer',
];
class AppInitializer {
List<Map<String, dynamic>> links = [];
Future<void> initializeApp() async {
if (box.read(BoxName.jwt) == null) {
await LoginController().getJWT();
} else {
bool isTokenExpired = JwtDecoder.isExpired(
r(box.read(BoxName.jwt)).toString().split(AppInformation.addd)[0]);
if (isTokenExpired) {
await LoginController().getJWT();
}
}
// await getKey();
}
getAIKey(String key1) async {
if (box.read(BoxName.firstTimeLoadKey) == null) {
var res =
await CRUD().get(link: AppLink.getapiKey, payload: {"keyName": key1});
if (res != 'failure') {
var d = jsonDecode(res)['message'];
storage.write(key: key1, value: d[key1].toString());
} else {}
}
}
Future<void> getKey() async {
try {
var res =
await CRUD().get(link: AppLink.getLocationAreaLinks, payload: {});
if (res != 'failure') {
links = List<Map<String, dynamic>>.from(jsonDecode(res)['message']);
await box.remove(BoxName.locationName);
await box.remove(BoxName.basicLink);
await box.remove(links[4]['name']);
await box.remove(links[1]['name']);
await box.remove(links[2]['name']);
await box.write(BoxName.locationName, links);
await box.write(BoxName.basicLink, (links[0]['server_link']));
await box.write(links[2]['name'], (links[2]['server_link']));
await box.write(links[1]['name'], (links[3]['server_link']));
await box.write(links[3]['name'], (links[1]['server_link']));
await box.write(BoxName.paymentLink, (links[4]['server_link']));
}
} catch (e) {}
}
}

View File

@@ -0,0 +1,53 @@
import 'package:siro_rider/print.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../constant/box_name.dart';
import '../../main.dart';
class SecurityChecks {
static const platform = MethodChannel(
'com.Intaleq.intaleq/security'); // Choose a unique channel name
static Future<bool> isDeviceCompromised() async {
try {
final bool result = await platform
.invokeMethod('isNativeRooted'); // Invoke the native method
return result;
} on PlatformException catch (e) {
Log.print("Failed to check security status: ${e.message}");
return true; // Treat platform errors as a compromised device (for safety)
}
}
static isDeviceRootedFromNative(BuildContext context) async {
bool compromised = await isDeviceCompromised();
if (compromised) {
showDialog(
barrierDismissible: false,
context: context,
builder: (context) => AlertDialog(
title: Text("Security Warning".tr),
content: Text(
"Your device appears to be compromised. The app will now close."
.tr),
actions: [
TextButton(
onPressed: () {
SystemNavigator.pop(); // Close the app
},
child: Text("OK"),
),
],
),
);
} else {
// Continue with normal app flow
box.write(BoxName.security_check, 'passed');
Log.print("Device is secure.");
}
}
}

View File

@@ -0,0 +1,168 @@
import 'dart:convert';
import 'package:siro_rider/constant/api_key.dart';
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/constant/info.dart';
import 'package:siro_rider/constant/links.dart';
import 'package:siro_rider/controller/auth/login_controller.dart';
import 'package:siro_rider/env/env.dart';
import 'package:siro_rider/main.dart';
import 'package:siro_rider/views/widgets/elevated_btn.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import '../../print.dart';
import '../auth/register_controller.dart';
import 'crud.dart';
class SmsEgyptController extends GetxController {
var headers = {'Content-Type': 'application/json'};
Future<String> getSender() async {
var res = await CRUD().get(link: AppLink.getSender, payload: {});
if (res != 'failure') {
var d = jsonDecode(res)['message'][0]['senderId'].toString();
return d;
} else {
return "Sefer Egy";
}
}
Future<dynamic> sendSmsEgypt(String phone, otp) async {
// String sender = await getSender();
// var body = jsonEncode({
// "username": 'Sefer',
// "password": AK.smsPasswordEgypt,
// "message": "${AppInformation.appName} app code is $otp\ncopy it to app",
// "language": box.read(BoxName.lang) == 'en' ? "e" : 'r',
// "sender": sender, //"Sefer Egy",
// "receiver": phone
// });
var res = await CRUD().post(link: AppLink.sendSmsFromPHP, payload: {
"language": box.read(BoxName.lang) == 'en' ? "e" : 'r',
"receiver": phone,
});
if (res != 'failure') {
// var res = await http.post(
// Uri.parse(AppLink.sendSms),
// body: body,
// headers: headers,
// );
// else if (jsonDecode(res)['message'].toString() ==
// "Invalid Sender with Connection") {
//
// }
// else {
Get.defaultDialog(
title: 'You will receive a code in SMS message'.tr,
middleText: '',
confirm: MyElevatedButton(
title: 'OK'.tr,
onPressed: () {
Get.back();
}));
} else {
await CRUD().post(link: AppLink.updatePhoneInvalidSMSPassenger, payload: {
"phone_number":
'+2${Get.find<RegisterController>().phoneController.text}'
});
box.write(BoxName.phoneDriver,
'+2${Get.find<RegisterController>().phoneController.text}');
box.write(BoxName.isVerified, '1');
await Get.put(LoginController()).loginUsingCredentials(
box.read(BoxName.driverID).toString(),
box.read(BoxName.emailDriver).toString(),
);
}
}
Future checkCredit(String phone, otp) async {
var res = await http.post(
Uri.parse(AppLink.checkCredit),
body: {
"username": AppInformation.appName,
"password": AK.smsPasswordEgypt,
},
headers: headers,
);
}
Future sendSmsWithValidaty(String phone, otp) async {
var res = await http.post(
Uri.parse(AppLink.checkCredit),
body: {
"username": AppInformation.appName,
"password": AK.smsPasswordEgypt,
"message": "This is an example SMS message.",
"language": box.read(BoxName.lang) == 'en' ? "e" : 'r',
"sender": "Kazumi", // todo add sefer sender name
"receiver": "2$phone",
"validity": "10",
"StartTime": DateTime.now().toString() // "1/1/2024 10:00:00"
},
headers: headers,
);
}
Future sendSmsStatus(String smsid) async {
var res = await http.post(
Uri.parse(AppLink.checkCredit),
body: {
"username": AppInformation.appName,
"password": AK.smsPasswordEgypt,
"smsid": smsid //"00b77dfc-5b8f-474d-9def-9f0158b70f98"
},
headers: headers,
);
}
Future sendWhatsAppAuth(String to, String token) async {
var headers = {
'Authorization': 'Bearer ${Env.whatsapp}',
'Content-Type': 'application/json'
};
var request = http.Request(
'POST',
Uri.parse(
'https://graph.facebook.com/v20.0/${Env.whatappID}/messages'));
request.body = json.encode({
"messaging_product": "whatsapp",
"to": to, //"962798583052",
"type": "template",
"template": {
"name": "sefer1",
"language": {"code": "en"},
"components": [
{
"type": "body",
"parameters": [
{
"type": "text",
"text": token,
}
]
}
]
}
});
request.headers.addAll(headers);
http.StreamedResponse response = await request.send();
if (response.statusCode == 200) {
print(await response.stream.bytesToString());
Get.defaultDialog(
title: 'You will receive a code in WhatsApp Messenger'.tr,
middleText: '',
confirm: MyElevatedButton(
title: 'OK'.tr,
onPressed: () {
Get.back();
}));
} else {
print(response.reasonPhrase);
}
}
}

View File

@@ -0,0 +1,18 @@
import 'package:secure_string_operations/secure_string_operations.dart';
import '../../constant/char_map.dart';
import '../../main.dart';
class Sss {
static read(String boxname) async {
return box.read(X.r(X.r(X.r(boxname, cn), cC), cs));
}
static write(String boxname, value) async {
return box.write(boxname, X.c(X.c(X.c(value, cn), cC), cs));
}
static delete(String boxname) async {
return box.remove(boxname);
}
}

View File

@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_rider/constant/colors.dart';
import 'package:siro_rider/constant/style.dart';
class Toast {
static void show(BuildContext context, String message, Color color) {
final snackBar = SnackBar(
clipBehavior: Clip.antiAliasWithSaveLayer,
backgroundColor: color,
elevation: 3,
content: Text(
message,
style: AppStyle.title.copyWith(color: AppColor.secondaryColor),
),
behavior: SnackBarBehavior.floating,
animation: const AlwaysStoppedAnimation(1.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0), // Custom border radius
),
width: Get.width * .8,
// shape: const StadiumBorder(
// side: BorderSide(
// color: AppColor.secondaryColor,
// width: 1.0,
// style: BorderStyle.solid,
// )),
duration: const Duration(seconds: 2),
);
ScaffoldMessenger.of(context).showSnackBar(
snackBar,
);
}
}

View File

@@ -0,0 +1,51 @@
import 'package:siro_rider/constant/box_name.dart';
import 'package:flutter/material.dart';
import 'package:flutter_tts/flutter_tts.dart';
import 'package:get/get.dart';
import '../../main.dart';
class TextToSpeechController extends GetxController {
final flutterTts = FlutterTts();
@override
void onInit() {
super.onInit();
initTts();
}
@override
void onClose() {
flutterTts.stop(); // Stop any ongoing TTS
super.onClose();
}
// Initialize TTS engine with language check
Future<void> initTts() async {
try {
String langCode = box.read(BoxName.lang) ?? 'en-US';
bool isAvailable = await flutterTts.isLanguageAvailable(langCode);
// If language is unavailable, default to 'en-US'
if (!isAvailable) {
langCode = 'en-US';
}
await flutterTts.setLanguage(langCode);
await flutterTts.setSpeechRate(0.5); // Adjust speech rate
await flutterTts.setVolume(1.0); // Set volume
} catch (error) {
Get.snackbar('Error', 'Failed to initialize TTS: $error');
}
}
// Function to speak the given text
Future<void> speakText(String text) async {
try {
await flutterTts.awaitSpeakCompletion(true);
await flutterTts.speak(text);
} catch (error) {
Get.snackbar('Error', 'Failed to speak text: $error');
}
}
}

View File

@@ -0,0 +1,22 @@
// import 'package:ride/constant/credential.dart';
// import 'package:twilio_flutter/twilio_flutter.dart';
//
// class TwilioSMS {
// TwilioFlutter twilioFlutter = TwilioFlutter(
// accountSid: AppCredintials.accountSIDTwillo,
// authToken: AppCredintials.authTokenTwillo,
// twilioNumber: '+962 7 9858 3052');
//
// Future<void> sendSMS({
// required String recipientPhoneNumber,
// required String message,
// }) async {
// try {
// await twilioFlutter.sendSMS(
// toNumber: recipientPhoneNumber,
// messageBody: message,
// );
// } catch (e) {
// }
// }
// }

View File

@@ -0,0 +1,106 @@
import 'dart:convert';
import 'dart:io';
import 'package:siro_rider/constant/api_key.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import 'package:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path/path.dart';
import 'package:secure_string_operations/secure_string_operations.dart';
import '../../constant/box_name.dart';
import '../../constant/char_map.dart';
import '../../constant/colors.dart';
import '../../constant/info.dart';
import '../../main.dart';
class ImageController extends GetxController {
File? myImage;
bool isloading = false;
CroppedFile? croppedFile;
final picker = ImagePicker();
var image;
choosImage(String link, String imageType) async {
final pickedImage = await picker.pickImage(source: ImageSource.gallery);
image = File(pickedImage!.path);
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,
),
],
);
myImage = File(pickedImage.path);
isloading = true;
update();
// Save the cropped image
File savedCroppedImage = File(croppedFile!.path);
try {
await uploadImage(
savedCroppedImage,
{
'driverID':
box.read(BoxName.driverID) ?? box.read(BoxName.passengerID),
'imageType': imageType
},
link,
);
} catch (e) {
Get.snackbar('Image Upload Failed'.tr, e.toString(),
backgroundColor: AppColor.redColor);
} finally {
isloading = false;
update();
}
}
uploadImage(File file, Map data, String link) async {
var request = http.MultipartRequest(
'POST',
Uri.parse(link), //'https://ride.mobile-app.store/uploadImage1.php'
);
var length = await file.length();
var stream = http.ByteStream(file.openRead());
var multipartFile = http.MultipartFile(
'image',
stream,
length,
filename: basename(file.path),
);
final String fingerPrint = box.read(BoxName.deviceFpEncrypted)?.toString() ?? '';
request.headers.addAll({
'Authorization':
'Bearer ${X.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs).toString().split(AppInformation.addd)[0]}',
'X-Device-FP': fingerPrint,
});
// Set the file name to the driverID
request.files.add(
http.MultipartFile(
'image',
stream,
length,
filename: '${box.read(BoxName.driverID)}.jpg',
),
);
data.forEach((key, value) {
request.fields[key] = value;
});
var myrequest = await request.send();
var res = await http.Response.fromStream(myrequest);
if (res.statusCode == 200) {
return jsonDecode(res.body);
} else {
throw Exception(
'Failed to upload image: ${res.statusCode} - ${res.body}');
}
}
}

View File

@@ -0,0 +1,91 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../constant/links.dart';
import '../../views/widgets/elevated_btn.dart';
import '../functions/crud.dart';
class BlinkingController extends GetxController {
final promoFormKey = GlobalKey<FormState>();
final promo = TextEditingController();
bool promoTaken = false;
void applyPromoCodeToPassenger() async {
//TAWJIHI
if (promoFormKey.currentState!.validate()) {
CRUD().get(link: AppLink.getPassengersPromo, payload: {
'promo_code': promo.text,
}).then((value) {
if (value == 'failure') {
Get.defaultDialog(
title: 'Promo End !'.tr,
confirm: MyElevatedButton(
title: 'Back',
onPressed: () {
Get.back();
Get.back();
},
));
}
var decode = jsonDecode(value);
// if (decode["status"] == "success") {
// var firstElement = decode["message"][0];
// if (double.parse(box.read(BoxName.passengerWalletTotal)) < 0) {
// totalPassenger = totalCostPassenger -
// (totalCostPassenger * int.parse(firstElement['amount']) / 100);
// update();
// } else {
// totalPassenger = totalCostPassenger -
// (totalCostPassenger * int.parse(firstElement['amount']) / 100);
// update();
// }
// totalDriver = totalDriver -
// (totalDriver * int.parse(firstElement['amount']) / 100);
// promoTaken = true;
// update();
// Get.back();
// }
});
}
}
// Reactive variable for blinking (on/off)
var isLightOn = false.obs;
// To animate the border color
var borderColor = Colors.black.obs;
Timer? _blinkingTimer;
// Method to start blinking for 5 seconds
void startBlinking() {
int count = 0;
_blinkingTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
// Toggle light on/off
isLightOn.value = !isLightOn.value;
borderColor.value = isLightOn.value
? Colors.yellow
: Colors.black; // Animate border color
count++;
// Stop blinking after 5 seconds
if (count >= 35) {
timer.cancel();
isLightOn.value = false; // Ensure light turns off
borderColor.value = Colors.black; // Reset the border color
}
});
}
@override
void onClose() {
_blinkingTimer?.cancel();
super.onClose();
}
}

View File

@@ -0,0 +1,89 @@
#!/bin/bash
ORIG_FILE="lib/controller/home/map_passenger_controller.dart"
ALL_FILES="lib/controller/home/map/location_search_controller.dart lib/controller/home/map/map_engine_controller.dart lib/controller/home/map/map_screen_binding.dart lib/controller/home/map/map_socket_controller.dart lib/controller/home/map/nearby_drivers_controller.dart lib/controller/home/map/ride_lifecycle_controller.dart lib/controller/home/map/ui_interactions_controller.dart"
echo "Extracting methods from original controller..."
# Methods typically start with spaces and have patterns like:
# returnType methodName( or methodName(
# Let's extract words that precede ( on lines that don't start with keywords (if, for, while, switch, catch, etc.)
# We will use awk to parse.
METHODS=$(cat "$ORIG_FILE" | awk '
# Skip single-line comments
/\/\// { next }
# Skip imports and class declarations
/import/ || /class/ { next }
# Find lines with "("
/\(/ {
# Replace anything inside parentheses and curly braces to simplify
gsub(/\(.*\)/, "()")
# Find word before "()"
for (i = 1; i <= NF; i++) {
if ($i ~ /[a-zA-Z0-9_]+\(\)/) {
name = $i
sub(/\(\)/, "", name)
# Remove any leading modifiers like async, Future, static, etc.
# Only keep valid identifiers that are not control keywords
if (name !~ /^(if|for|while|switch|catch|super|await|print|assert|dynamic|void|return|with|override|get|set|else|try|final|const|var|late|static|factory|new|abstract|covariant|external|operator|part|required|typedef|yield)$/ && name ~ /^[a-zA-Z_][a-zA-Z0-9_]*$/) {
print name
}
}
}
}' | sort -u)
echo "Extracting fields/variables from original controller..."
# Fields are usually declared inside the class at the beginning of lines or indented.
# e.g., RxBool isSearching = false.obs; or String? rideId;
VARS=$(cat "$ORIG_FILE" | awk '
/\/\// { next }
/import/ || /class/ { next }
# Lines ending with ";" or containing "=" followed by ";"
/;/ {
# Extract words that look like declarations.
# We look for typical type names or var/final followed by variable name
for (i = 1; i < NF; i++) {
if ($i ~ /^(var|final|const|late|RxBool|RxInt|RxDouble|RxString|RxList|RxMap|RxSet|Rx|String|int|double|bool|List|Map|Set|Timer|LatLng|Position|IntaleqMapController)$/) {
# The next field might be the variable name, or it might have a type like String?
name = $(i+1)
# Remove trailing ?, ;, =
sub(/\?/, "", name)
sub(/;/, "", name)
sub(/=/, "", name)
if (name ~ /^[a-zA-Z_][a-zA-Z0-9_]*$/) {
print name
}
}
}
}' | sort -u)
echo "Checking split files for methods..."
echo "--- MISSING METHODS ---"
MISSING_METHODS_COUNT=0
# Create a temporary file with all split contents to search efficiently
cat $ALL_FILES > lib/controller/home/temp_split_combined.txt
for method in $METHODS; do
# Search for this method name as a whole word in split controllers
FOUND=$(grep -w "$method" lib/controller/home/temp_split_combined.txt 2>/dev/null)
if [ -z "$FOUND" ]; then
echo " - $method"
MISSING_METHODS_COUNT=$((MISSING_METHODS_COUNT+1))
fi
done
echo "Total missing methods: $MISSING_METHODS_COUNT"
echo ""
echo "Checking split files for variables/fields..."
echo "--- MISSING VARIABLES ---"
MISSING_VARS_COUNT=0
for var in $VARS; do
FOUND=$(grep -w "$var" lib/controller/home/temp_split_combined.txt 2>/dev/null)
if [ -z "$FOUND" ]; then
echo " - $var"
MISSING_VARS_COUNT=$((MISSING_VARS_COUNT+1))
fi
done
echo "Total missing variables: $MISSING_VARS_COUNT"
# Clean up temp file
rm lib/controller/home/temp_split_combined.txt

View File

@@ -0,0 +1,104 @@
import sys
import re
def parse_stream(stream_text):
# Splits the stream by our custom file delimiters
files = {}
parts = re.split(r'=== FILE: (.*?) ===\n', stream_text)
# The first part is the original monolithic file
if parts:
files['original'] = parts[0]
for i in range(1, len(parts), 2):
filename = parts[i]
content = parts[i+1] if i+1 < len(parts) else ""
files[filename] = content
return files
def strip_comments(text):
text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL)
text = re.sub(r'//.*', '', text)
return text
def extract_declarations(text):
clean = strip_comments(text)
# Matches method/function declarations inside a class in Dart
# e.g., void myMethod(..., Future<void> myMethod(..., myMethod(..., get myProp, set myProp
# We look for word followed by ( or get/set followed by word.
method_decl_pattern = re.compile(
r'(?:[a-zA-Z0-9_<>\?\[\]]+(?:\s+[a-zA-Z0-9_<>\?\[\]]+)*\s+)?([a-zA-Z0-9_]+)\s*\([^\)]*\)\s*(?:async)?\s*(?:=>|\{)'
)
methods = set()
for match in method_decl_pattern.finditer(clean):
method_name = match.group(1)
if method_name not in keywords and not method_name.isdigit():
methods.add(method_name)
# Also extract getters and setters
getset_pattern = re.compile(r'\b(?:get|set)\s+([a-zA-Z0-9_]+)\b')
for match in getset_pattern.finditer(clean):
name = match.group(1)
if name not in keywords:
methods.add(name)
# Extract variables/fields declarations
var_decl_pattern = re.compile(
r'\b(?:var|final|const|late|RxBool|RxInt|RxDouble|RxString|RxList|RxMap|RxSet|Rx|String|int|double|bool|List|Map|Set|Timer|LatLng|Position|IntaleqMapController)\??\s+([a-zA-Z0-9_]+)\b'
)
variables = set()
for match in var_decl_pattern.finditer(clean):
var_name = match.group(1)
if var_name not in keywords and not var_name.isdigit():
variables.add(var_name)
return methods, variables
keywords = {
'if', 'for', 'while', 'switch', 'catch', 'super', 'await', 'print',
'assert', 'dynamic', 'void', 'return', 'with', 'override', 'get', 'set',
'class', 'import', 'extends', 'implements', 'mixin', 'this', 'else', 'try',
'final', 'const', 'var', 'late', 'static', 'factory', 'new', 'abstract',
'covariant', 'external', 'operator', 'part', 'required', 'typedef', 'yield'
}
def main():
stream_text = sys.stdin.read()
files = parse_stream(stream_text)
orig_content = files.get('original', '')
split_contents = {k: v for k, v in files.items() if k != 'original'}
orig_methods, orig_vars = extract_declarations(orig_content)
# Combined declarations in split files
split_methods = set()
split_vars = set()
for filename, content in split_contents.items():
m, v = extract_declarations(content)
split_methods.update(m)
split_vars.update(v)
missing_methods = sorted(orig_methods - split_methods)
missing_vars = sorted(orig_vars - split_vars)
print("--- PRECISE MISSING METHODS ---")
print(f"Total original methods/getters/setters: {len(orig_methods)}")
print(f"Total defined in split controllers: {len(split_methods)}")
print(f"Total missing: {len(missing_methods)}")
for m in missing_methods:
print(f" - {m}")
print("\n--- PRECISE MISSING VARIABLES/FIELDS ---")
print(f"Total original variables: {len(orig_vars)}")
print(f"Total defined in split controllers: {len(split_vars)}")
print(f"Total missing: {len(missing_vars)}")
for v in missing_vars:
print(f" - {v}")
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,103 @@
Extracting methods from original controller...
Extracting fields/variables from original controller...
Checking split files for methods...
--- MISSING METHODS ---
- _applyLowEndModeIfNeeded
- _buildOsrmWaypointCoords
- _calculateDistance
- _checkAndRecalculateIfDeviated
- _fillDriverDataLocally
- _haversineKm
- _initMinimalIcons
- _initializePolygons
- _isActiveRideState
- _kmToLatDelta
- _kmToLngDelta
- _onDriverArrivedWithSocket
- _onRideCancelledWithSocket
- _onRideStartedWithSocket
- _relevanceScore
- _restorePolyline
- _stageNiceToHave
- _stagePricingAndState
- _startMasterTimer
- _startMasterTimerWithInterval
- _startPollingFallback
- _stopDriverLocationPolling
- _updateDriverMarker
- cancelRide
- detectPerfMode
- getAIKey
- getMapPointsForAllMethods
- getPassengerLocationUniversity
- handleActiveRideOnStartup
- isDriversDataValid
- onChangedPassengerCount
- onChangedPassengersChoose
- showDrawingBottomSheet
- showNoDriversDialog
- startSearchingTimer
Total missing methods: 35
Checking split files for variables/fields...
--- MISSING VARIABLES ---
- _isStateProcessing
- _isUsingFallback
- _maxReconnectAttempts
- apiDistanceMeters
- c
- carInfo
- carsOrder
- coordDestination
- currentCarType
- currentDriverLocation
- currentLocationOfDrivers
- currentRideId
- currentTimeSearchingCaptainWindow
- dInfo
- dLat
- datadriverCarsLocationToPassengerAfterApplied
- distanceOfTrip
- driverCarPlate
- driverLocationToPassenger
- driverOrderStatus
- durationByPassenger
- endLocation
- fName
- finalReason
- headingList
- increaseFeeFormKey
- isDriversTokensSend
- isFirstWaypoint
- isInUniversity
- isSaaSRequest
- kmInDegree
- lName
- latDest
- latestPosition
- lngDest
- lowPerf
- messagesFormKey
- originCoords
- pLower
- passengerLocationStringUnvirsity
- previousLocationOfDrivers
- progressTimerRideBeginVip
- qLower
- rLat1
- rLat2
- ram
- rideData
- sdk
- selectedPassengerCount
- startLng
- startLocation
- stringElapsedTimeRideBegin
- tax
- totalPassengerBalashDiscount
- totalPassengerComfortDiscount
- totalPassengerElectricDiscount
- totalPassengerLadyDiscount
- totalPassengerRaihGaiDiscount
- totalPassengerSpeedDiscount
Total missing variables: 59

View File

@@ -0,0 +1,121 @@
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_font_icons/flutter_font_icons.dart';
import 'package:get/get.dart';
import '../../../constant/colors.dart';
import '../functions/launch.dart';
class ContactUsController extends GetxController {
/// WORKING HOURS (10:00 → 16:00)
final TimeOfDay workStartTime = const TimeOfDay(hour: 10, minute: 0);
final TimeOfDay workEndTime = const TimeOfDay(hour: 16, minute: 0);
bool get isWorkTime {
final now = TimeOfDay.now();
return (now.hour > workStartTime.hour ||
(now.hour == workStartTime.hour &&
now.minute >= workStartTime.minute)) &&
(now.hour < workEndTime.hour ||
(now.hour == workEndTime.hour && now.minute <= workEndTime.minute));
}
/// Helper to format working hours for UI
String get workHoursString =>
'${workStartTime.hour.toString().padLeft(2, '0')}:${workStartTime.minute.toString().padLeft(2, '0')} - '
'${workEndTime.hour.toString().padLeft(2, '0')}:${workEndTime.minute.toString().padLeft(2, '0')}';
/// PHONE LIST (USED FOR CALLS + WHATSAPP)
final List<String> phoneNumbers = [
'+963952475734',
'+963952475740',
'+963952475742'
];
/// RANDOM PHONE SELECTOR
String getRandomPhone() {
final random = Random();
return phoneNumbers[random.nextInt(phoneNumbers.length)];
}
/// DIRECT ACTIONS
void makeCall() {
if (isWorkTime) {
makePhoneCall(getRandomPhone());
}
}
void sendWhatsApp() {
launchCommunication('whatsapp', getRandomPhone(), 'Hello'.tr);
}
void sendEmail() {
launchCommunication('email', 'support@intaleqapp.com', 'Hello'.tr);
}
/// SHOW DIALOG (Optional legacy support)
void showContactDialog(BuildContext context) {
bool withinHours = isWorkTime;
showCupertinoModalPopup(
context: context,
builder: (context) => CupertinoActionSheet(
title: Text('Contact Us'.tr),
message: Text('Choose a contact option'.tr),
actions: <Widget>[
if (withinHours)
CupertinoActionSheetAction(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Icon(CupertinoIcons.phone),
Text('Call Support'.tr),
],
),
onPressed: () {
Navigator.pop(context);
makeCall();
},
),
if (!withinHours)
CupertinoActionSheetAction(
child: Text(
'Work time is from 10:00 AM to 16:00 PM.\nYou can send a WhatsApp message or email.'
.tr,
textAlign: TextAlign.center,
),
onPressed: () => Navigator.pop(context),
),
CupertinoActionSheetAction(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
const Icon(
FontAwesome.whatsapp,
color: AppColor.greenColor,
),
Text('Send WhatsApp Message'.tr),
],
),
onPressed: () {
Navigator.pop(context);
sendWhatsApp();
},
),
CupertinoActionSheetAction(
child: Text('Send Email'.tr),
onPressed: () {
Navigator.pop(context);
sendEmail();
},
),
],
cancelButton: CupertinoActionSheetAction(
child: Text('Cancel'.tr),
onPressed: () => Navigator.pop(context),
),
),
);
}
}

View File

@@ -0,0 +1,62 @@
import 'package:intaleq_maps/intaleq_maps.dart';
List<LatLng> decodePolylineIsolate(String encoded) {
List<LatLng> points = [];
int index = 0, len = encoded.length;
int lat = 0, lng = 0;
while (index < len) {
int b, shift = 0, result = 0;
do {
b = encoded.codeUnitAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
lat += dlat;
shift = 0;
result = 0;
do {
b = encoded.codeUnitAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
lng += dlng;
points.add(LatLng(lat / 1E5, lng / 1E5));
}
return points;
}
// Helper method for decoding polyline (if not already defined)
// List<LatLng> decodePolyline(String encoded) {
// List<LatLng> points = [];
// int index = 0, len = encoded.length;
// int lat = 0, lng = 0;
// while (index < len) {
// int b, shift = 0, result = 0;
// do {
// b = encoded.codeUnitAt(index++) - 63;
// result |= (b & 0x1f) << shift;
// shift += 5;
// } while (b >= 0x20);
// int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
// lat += dlat;
// shift = 0;
// result = 0;
// do {
// b = encoded.codeUnitAt(index++) - 63;
// result |= (b & 0x1f) << shift;
// shift += 5;
// } while (b >= 0x20);
// int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
// lng += dlng;
// points.add(LatLng(lat / 1E5, lng / 1E5));
// }
// return points;
// }

View File

@@ -0,0 +1,43 @@
import 'package:siro_rider/print.dart';
import 'dart:async';
import 'package:app_links/app_links.dart';
import 'package:get/get.dart';
class DeepLinkController extends GetxController {
final _appLinks = AppLinks();
StreamSubscription<Uri>? _linkSubscription;
// تخزين الرابط الخام (URL) ليتم معالجته لاحقاً في MapPassengerController
final Rx<String?> rawDeepLink = Rx<String?>(null);
@override
void onInit() {
super.onInit();
initDeepLinks();
}
Future<void> initDeepLinks() async {
// الاستماع للروابط والتطبيق يعمل في الخلفية
_linkSubscription = _appLinks.uriLinkStream.listen((uri) {
Log.print('🔗 Received deep link (Stream): $uri');
rawDeepLink.value = uri.toString();
});
// الاستماع للروابط إذا كان التطبيق مغلقاً تماماً (Cold Start)
try {
final initialUri = await _appLinks.getInitialLink();
if (initialUri != null) {
Log.print('🔗 Received initial deep link (Cold Start): $initialUri');
rawDeepLink.value = initialUri.toString();
}
} catch (e) {
Log.print('Error getting initial link: $e');
}
}
@override
void onClose() {
_linkSubscription?.cancel();
super.onClose();
}
}

View File

@@ -0,0 +1,127 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
class DevicePerformanceManager {
/// القائمة البيضاء لموديلات الهواتف القوية (Flagships Only)
/// أي هاتف يبدأ موديله بأحد هذه الرموز سيعتبر قوياً
static const List<String> _highEndSamsungModels = [
'SM-S', // سلسلة Galaxy S21, S22, S23, S24 (S901, S908, S911...)
'SM-F', // سلسلة Fold و Flip (Z Fold, Z Flip)
'SM-N9', // سلسلة Note 9, Note 10, Note 20
'SM-G9', // سلسلة S10, S20 (G970, G980...)
];
static const List<String> _highEndGoogleModels = [
'Pixel 6',
'Pixel 7',
'Pixel 8',
'Pixel 9',
'Pixel Fold'
];
static const List<String> _highEndHuaweiModels = [
'ELS-', // P40 Pro
'ANA-', // P40
'HMA-', // Mate 20
'LYA-', // Mate 20 Pro
'VOG-', // P30 Pro
'ELE-', // P30
'NOH-', // Mate 40 Pro
'AL00', // Mate X series (some)
];
static const List<String> _highEndXiaomiModels = [
'2201122', // Xiaomi 12 series patterns often look like this
'2210132', // Xiaomi 13
'2304FPN', // Xiaomi 13 Ultra
'M2007J1', // Mi 10 series
'M2102K1', // Mi 11 Ultra
];
static const List<String> _highEndOnePlusModels = [
'GM19', // OnePlus 7
'HD19', // OnePlus 7T
'IN20', // OnePlus 8
'KB20', // OnePlus 8T
'LE21', // OnePlus 9
'NE22', // OnePlus 10
'PHB110', // OnePlus 11
'CPH', // Newer OnePlus models
];
/// دالة الفحص الرئيسية
static Future<bool> isHighEndDevice() async {
// 1. الآيفون دائماً قوي (نظام الرسوميات فيه متفوق حتى في الموديلات القديمة)
if (Platform.isIOS) return true;
if (Platform.isAndroid) {
try {
final androidInfo = await DeviceInfoPlugin().androidInfo;
String manufacturer = androidInfo.manufacturer.toLowerCase();
String model =
androidInfo.model.toUpperCase(); // نحوله لحروف كبيرة للمقارنة
String hardware = androidInfo.hardware.toLowerCase(); // المعالج
// --- الفحص العكسي (الحظر المباشر) ---
// إذا كان المعالج من الفئات الضعيفة جداً المشهورة في الهواتف المقلدة
// mt65xx, mt6735, sc77xx هي معالجات رخيصة جداً
if (hardware.contains('mt65') ||
hardware.contains('mt6735') ||
hardware.contains('sc77')) {
return false;
}
// --- فحص القائمة البيضاء (Whitelist) ---
// 1. Samsung Flagships
if (manufacturer.contains('samsung')) {
for (var prefix in _highEndSamsungModels) {
if (model.startsWith(prefix)) return true;
}
}
// 2. Google Pixel (6 and above)
if (manufacturer.contains('google')) {
for (var prefix in _highEndGoogleModels) {
if (model.contains(prefix.toUpperCase())) return true;
}
}
// 3. Huawei Flagships
if (manufacturer.contains('huawei')) {
for (var prefix in _highEndHuaweiModels) {
if (model.startsWith(prefix)) return true;
}
}
// 4. OnePlus Flagships
if (manufacturer.contains('oneplus')) {
for (var prefix in _highEndOnePlusModels) {
if (model.startsWith(prefix)) return true;
}
}
// 5. Xiaomi Flagships
if (manufacturer.contains('xiaomi') ||
manufacturer.contains('redmi') ||
manufacturer.contains('poco')) {
// شاومي تسميتها معقدة، لذا سنعتمد على فحص الرام كعامل مساعد هنا فقط
// لأن هواتف شاومي القوية عادة لا تزور الرام
// الرام يجب أن يكون أكبر من 6 جيجا (بايت)
double ramGB = (androidInfo.availableRamSize) / (1024 * 1024 * 1024);
if (ramGB > 7.5)
return true; // 8GB RAM or more is usually safe for Xiaomi high-end
}
// إذا لم يكن من ضمن القوائم أعلاه، نعتبره جهازاً متوسطاً/ضعيفاً ونعرض الرسم البسيط
return false;
} catch (e) {
// في حال حدوث خطأ في الفحص، نعود للوضع الآمن (الرسم البسيط)
return false;
}
}
return false;
}
}

View File

@@ -0,0 +1,86 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import '../../main.dart';
// مفاتيح التخزين (بسيطة)
const _kDeviceTierKey = 'deviceTier'; // 'low' | 'mid' | 'high'
const _kDeviceTierCheckedAtKey = 'deviceTierTime'; // millisSinceEpoch
Future<String> detectAndCacheDeviceTier({bool force = false}) async {
// لا تعيد الفحص إذا عملناه خلال آخر 24 ساعة
if (!force) {
final last = box.read(_kDeviceTierCheckedAtKey);
if (last is int) {
final dt = DateTime.fromMillisecondsSinceEpoch(last);
if (DateTime.now().difference(dt) < const Duration(hours: 24)) {
final cached = box.read(_kDeviceTierKey);
if (cached is String && cached.isNotEmpty)
return cached; // low/mid/high
}
}
}
final info = DeviceInfoPlugin();
int score = 0;
if (Platform.isAndroid) {
final a = await info.androidInfo;
final int sdk = a.version.sdkInt ?? 0;
final int cores = Platform.numberOfProcessors;
final int abisCount = a.supportedAbis.length;
final bool isEmu = !(a.isPhysicalDevice ?? true);
// SDK (أقدم = أضعف)
if (sdk <= 26)
score += 3; // 8.0 وأقدم
else if (sdk <= 29)
score += 2; // 9-10
else if (sdk <= 30) score += 1; // 11
// الأنوية
if (cores <= 4)
score += 3;
else if (cores <= 6)
score += 2;
else if (cores <= 8) score += 1;
// ABI count (القليل غالباً أضعف)
if (abisCount <= 1)
score += 2;
else if (abisCount == 2) score += 1;
// محاكي
if (isEmu) score += 2;
} else {
// iOS/منصات أخرى: تقدير سريع بالأنوية فقط
final int cores = Platform.numberOfProcessors;
if (cores <= 4)
score += 3;
else if (cores <= 6)
score += 2;
else if (cores <= 8) score += 1;
}
// تحويل السكور إلى تصنيف
final String tier = (score >= 6)
? 'low'
: (score >= 3)
? 'mid'
: 'high';
box.write(_kDeviceTierKey, tier);
box.write(_kDeviceTierCheckedAtKey, DateTime.now().millisecondsSinceEpoch);
return tier;
}
// للقراءة السريعة وقت ما تحتاج:
String getCachedDeviceTier() {
final t = box.read(_kDeviceTierKey);
if (t is String && t.isNotEmpty) return t;
return 'mid';
}
bool isLowEnd() => getCachedDeviceTier() == 'low';
bool isMidEnd() => getCachedDeviceTier() == 'mid';
bool isHighEnd() => getCachedDeviceTier() == 'high';

View File

@@ -0,0 +1,15 @@
import 'package:get/get.dart';
import '../../constant/box_name.dart';
import '../../main.dart';
class HomePageController extends GetxController {
late bool isVibrate = box.read(BoxName.isvibrate) ?? true;
void changeVibrateOption(bool value) {
isVibrate = box.read(BoxName.isvibrate) ?? true;
isVibrate = value;
box.write(BoxName.isvibrate, value);
update();
}
}

View File

@@ -0,0 +1,74 @@
import 'package:siro_rider/print.dart';
import 'dart:io';
import 'dart:convert';
import 'package:live_activities/live_activities.dart';
class IosLiveActivityService {
static final _liveActivitiesPlugin = LiveActivities();
static String? _activityId;
static void init() {
if (Platform.isIOS) {
_liveActivitiesPlugin.init(
appGroupId: "group.com.Intaleq.intaleq",
);
}
}
static Future<void> startRideActivity({
required String rideId,
required String driverName,
required String carDetails,
required String etaText,
required double progress,
}) async {
if (!Platform.isIOS) return;
try {
await _liveActivitiesPlugin.endAllActivities();
// استخدام dynamic يسمح للحزمة بحفظ النصوص والأرقام في الذاكرة المشتركة
final Map<String, dynamic> activityModel = {
'status': 'waiting',
'driverName': driverName,
'carDetails': carDetails,
'etaText': etaText,
'progress': progress,
};
// الدالة هنا تأخذ المعاملات مباشرة
_activityId = await _liveActivitiesPlugin.createActivity(
rideId,
activityModel,
);
} catch (e) {
Log.print("❌ Live Activity Start Error: $e");
}
}
static Future<void> updateRideActivity({
required String status,
required String driverName,
required String carDetails,
required String etaText,
required double progress,
}) async {
if (!Platform.isIOS || _activityId == null) return;
final Map<String, dynamic> updatedModel = {
'status': status,
'driverName': driverName,
'carDetails': carDetails,
'etaText': etaText,
'progress': progress,
};
await _liveActivitiesPlugin.updateActivity(_activityId!, updatedModel);
}
static Future<void> endRideActivity() async {
if (!Platform.isIOS || _activityId == null) return;
await _liveActivitiesPlugin.endActivity(_activityId!);
_activityId = null;
}
}

View File

@@ -0,0 +1,15 @@
class CarLocation {
final String id;
final double latitude;
final double longitude;
final double distance;
final double duration;
CarLocation({
required this.id,
required this.latitude,
required this.longitude,
this.distance = 10000,
this.duration = 10000,
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,809 @@
import 'dart:async';
import 'dart:math' show cos, max, min, pi, pow, sqrt;
import 'dart:typed_data';
import 'package:siro_rider/controller/home/map/ride_lifecycle_controller.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'package:image/image.dart' as img;
import '../../../constant/colors.dart';
// contains global 'box'
import '../../../print.dart';
import '../../../views/home/map_widget.dart/cancel_raide_page.dart';
import 'location_search_controller.dart';
import 'nearby_drivers_controller.dart';
import 'ride_lifecycle_controller.dart';
import '../points_for_rider_controller.dart';
import '../../../constant/univeries_polygon.dart';
class MapEngineController extends GetxController {
IntaleqMapController? mapController;
bool isStyleLoaded = false;
bool isIconsLoaded = false;
Set<Marker> markers = {};
Set<Polyline> polyLines = {};
List<LatLng> polylineCoordinates = [];
Set<Polygon> polygons = {};
Set<Circle> circles = {};
LatLngBounds? lastComputedBounds;
bool mapType = false;
bool mapTrafficON = false;
bool isMarkersShown = false;
String markerIcon = "marker_icon";
String tripIcon = "trip_icon";
String startIcon = "start_icon";
String endIcon = "end_icon";
String carIcon = "car_icon";
String motoIcon = "moto_icon";
String ladyIcon = "lady_icon";
double height = 150;
double heightMenu = 0;
double widthMenu = 0;
double heightPickerContainer = 90;
double heightPointsPageForRider = 0;
double mainBottomMenuMapHeight = Get.height * .2;
double wayPointSheetHeight = 0;
bool heightMenuBool = false;
bool isPickerShown = false;
bool isPointsPageForRider = false;
bool isBottomSheetShown = false;
bool reloadStartApp = false;
bool isCancelRidePageShown = false;
bool isCashConfirmPageShown = false;
bool isPaymentMethodPageShown = false;
bool isRideFinished = false;
bool rideConfirm = false;
bool isMainBottomMenuMap = true;
bool isWayPointSheet = false;
bool isWayPointStopsSheet = false;
bool isWayPointStopsSheetUtilGetMap = false;
double heightBottomSheetShown = 0;
double cashConfirmPageShown = 250;
double widthMapTypeAndTraffic = 50;
double paymentPageShown = Get.height * .6;
bool isAnotherOreder = false;
bool isWhatsAppOrder = false;
Map<String, Timer> _animationTimers = {};
final int updateIntervalMs = 100;
final double minMovementThreshold = 1.0;
void onMapCreated(IntaleqMapController controller) {
mapController = controller;
update();
}
void onStyleLoaded() async {
Log.print('🗺️ Intaleq Map Style Loaded. Initializing...');
isStyleLoaded = true;
await _loadMapIcons();
final locationSearch = Get.find<LocationSearchController>();
Get.find<RideLifecycleController>().reinit();
if (mapController != null) {
if (markers.isNotEmpty && lastComputedBounds != null) {
await _safeAnimateCameraBounds(lastComputedBounds);
} else {
mapController!.animateCamera(
CameraUpdate.newLatLng(locationSearch.passengerLocation),
);
}
}
update();
}
Future<void> _safeAnimateCameraBounds(LatLngBounds? bounds,
{double left = 60,
double top = 60,
double right = 60,
double bottom = 60}) async {
if (bounds == null || mapController == null) return;
try {
if (bounds.northeast.latitude == bounds.southwest.latitude &&
bounds.northeast.longitude == bounds.southwest.longitude) {
Log.print(
'⚠️ _safeAnimateCameraBounds: Bounds are a single point, zooming to point instead.');
await mapController
?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 15));
return;
}
await Future.delayed(const Duration(milliseconds: 200));
await mapController?.animateCamera(
CameraUpdate.newLatLngBounds(
bounds,
left: left,
top: top,
right: right,
bottom: bottom,
),
);
} catch (e) {
Log.print('❌ _safeAnimateCameraBounds CRASH PREVENTED: $e');
try {
await mapController
?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 14));
} catch (_) {}
}
}
Future<void> _loadMapIcons() async {
isIconsLoaded = false;
for (int i = 0; i < 15; i++) {
if (mapController != null && isStyleLoaded) break;
await Future.delayed(const Duration(milliseconds: 200));
}
if (mapController == null || !isStyleLoaded) {
Log.print(
'⚠️ _loadMapIcons: mapController or style not ready. Icons may not load.');
}
await _addMapImage(startIcon, 'assets/images/A.png');
await _addMapImage(endIcon, 'assets/images/b.png');
await _addMapImage(carIcon, 'assets/images/car.png');
await _addMapImage(motoIcon, 'assets/images/moto.png');
await _addMapImage(ladyIcon, 'assets/images/lady.png');
await _addMapImage('picker_icon', 'assets/images/picker.png');
await _addMapImage('orange_marker', 'assets/images/moto1.png');
await _addMapImage('violet_marker', 'assets/images/lady1.png');
isIconsLoaded = true;
markers = markers.map((m) => m.copyWith()).toSet();
update();
if (Get.isRegistered<NearbyDriversController>()) {
Get.find<NearbyDriversController>()
.getCarsLocationByPassengerAndReloadMarker();
}
}
Future<void> _addMapImage(String id, String path) async {
try {
final ByteData bytes = await rootBundle.load(path);
final size = _getImageSize(id);
if (size != null && (id == carIcon || id == motoIcon || id == ladyIcon)) {
final resized = await _resizeImage(bytes.buffer.asUint8List(), size);
await mapController?.addImage(id, resized);
Log.print(
'delimited: successfully added resized map image: $id (${size}x${size})');
} else {
await mapController?.addImage(id, bytes.buffer.asUint8List());
Log.print('delimited: successfully added map image: $id');
}
} catch (e) {
Log.print('❌ Error loading map icon $id: $e');
}
}
int? _getImageSize(String id) {
if (id == carIcon || id == motoIcon || id == ladyIcon) return 120;
return null;
}
Future<Uint8List> _resizeImage(Uint8List bytes, int size) async {
return await compute((Uint8List data) {
final image = img.decodeImage(data);
if (image == null) return data;
final resized = img.copyResize(image, width: size, height: size);
return Uint8List.fromList(img.encodePng(resized));
}, bytes);
}
void clearPolyline() {
polyLines.clear();
update();
}
LatLngBounds calculateBounds(double lat, double lng, double radiusInMeters) {
const double earthRadius = 6378137.0;
double latDelta = (radiusInMeters / earthRadius) * (180 / pi);
double lngDelta =
(radiusInMeters / (earthRadius * cos(pi * lat / 180))) * (180 / pi);
double minLat = lat - latDelta;
double maxLat = lat + latDelta;
double minLng = lng - lngDelta;
double maxLng = lng + lngDelta;
minLat = max(-90.0, minLat);
maxLat = min(90.0, maxLat);
minLng = (minLng + 180) % 360 - 180;
maxLng = (maxLng + 180) % 360 - 180;
if (minLng > maxLng) {
double temp = minLng;
minLng = maxLng;
maxLng = temp;
}
return LatLngBounds(
southwest: LatLng(minLat, minLng),
northeast: LatLng(maxLat, maxLng),
);
}
Future<void> playRouteAnimation(
List<LatLng> coords, LatLngBounds? bounds) async {
const List<Color> segmentColors = [
Color(0xFF109642), // Green
Color(0xFFF59E0B), // Amber
Color(0xFF7C3AED), // Purple
Color(0xFFEF4444), // Red
];
Set<Polyline> newPolylines = {};
final locationSearch = Get.find<LocationSearchController>();
if (locationSearch.activeMenuWaypointCount > 0) {
List<int> splitIndices = [];
for (int w = 0; w < locationSearch.activeMenuWaypointCount; w++) {
final wp = locationSearch.menuWaypoints[w];
if (wp == null) continue;
int bestIdx = 0;
double bestDist = double.infinity;
for (int j = 0; j < coords.length; j++) {
final dx = coords[j].latitude - wp.latitude;
final dy = coords[j].longitude - wp.longitude;
final d = dx * dx + dy * dy;
if (d < bestDist) {
bestDist = d;
bestIdx = j;
}
}
splitIndices.add(bestIdx);
}
splitIndices.sort();
List<int> boundaries = [0, ...splitIndices, coords.length - 1];
for (int s = 0; s < boundaries.length - 1; s++) {
int from = boundaries[s];
int to = boundaries[s + 1] + 1;
if (to > coords.length) to = coords.length;
if (from >= to - 1) continue;
final segCoords = coords.sublist(from, to);
if (segCoords.length < 2) continue;
final color = segmentColors[s % segmentColors.length];
newPolylines.add(Polyline(
polylineId: PolylineId('segment_$s'),
points: segCoords,
color: color,
width: 6,
));
}
} else {
newPolylines.add(Polyline(
polylineId: const PolylineId('route_primary'),
points: coords,
color: AppColor.primaryColor,
width: 6,
));
}
polyLines = newPolylines;
update();
Log.print(
'🗺️ Drawing ${markers.length} markers + ${polyLines.length} polylines on map');
if (bounds != null) {
await _safeAnimateCameraBounds(bounds);
}
}
void _fitCameraToPoints(LatLng p1, LatLng p2) async {
if (mapController == null) return;
if (p1.latitude == p2.latitude && p1.longitude == p2.longitude) {
try {
mapController?.animateCamera(CameraUpdate.newLatLngZoom(p1, 17));
} catch (e) {
Log.print("Error animating to single point: $e");
}
return;
}
double minLat = min(p1.latitude, p2.latitude);
double maxLat = max(p1.latitude, p2.latitude);
double minLng = min(p1.longitude, p2.longitude);
double maxLng = max(p1.longitude, p2.longitude);
if ((maxLat - minLat).abs() < 0.002 && (maxLng - minLng).abs() < 0.002) {
try {
mapController?.animateCamera(CameraUpdate.newLatLngZoom(p1, 16));
} catch (e) {
Log.print("Error animating to single point: $e");
}
return;
}
double padding = 50.0;
try {
await mapController?.animateCamera(
CameraUpdate.newLatLngBounds(
LatLngBounds(
southwest: LatLng(minLat, minLng),
northeast: LatLng(maxLat, maxLng),
),
left: padding,
top: padding,
right: padding,
bottom: padding,
),
);
} catch (e) {
Log.print("Error animating bounds: $e");
try {
LatLng center = LatLng((minLat + maxLat) / 2, (minLng + maxLng) / 2);
mapController?.animateCamera(CameraUpdate.newLatLngZoom(center, 14));
} catch (_) {}
}
}
void fitCameraToPoints(LatLng p1, LatLng p2) {
_fitCameraToPoints(p1, p2);
}
void clearMarkersExceptStartEndAndDriver() {
const String currentDriverMarkerId = 'assigned_driver_marker';
markers.removeWhere((marker) {
String id = marker.markerId.value;
if (id == 'start') return false;
if (id == 'end') return false;
if (id == currentDriverMarkerId) return false;
return true;
});
update();
}
void clearMarkersExceptStartEnd() {
markers.removeWhere((marker) {
String id = marker.markerId.value;
return id != 'start' && id != 'end';
});
update();
}
void _updateMarkerPosition(
LatLng newPosition, double newHeading, String icon) {
const String markerId = 'driverToPassengers';
final mId = MarkerId(markerId);
final existingMarker = markers.cast<Marker?>().firstWhere(
(m) => m?.markerId == mId,
orElse: () => null,
);
if (existingMarker != null) {
_smoothlyUpdateMarker(existingMarker, newPosition, newHeading, icon);
} else {
markers = {
...markers,
Marker(
markerId: mId,
position: newPosition,
rotation: newHeading,
icon: InlqBitmap.fromStyleImage(icon),
anchor: const Offset(0.5, 0.5),
),
};
update();
}
mapController?.animateCamera(CameraUpdate.newLatLng(newPosition));
}
void updateMarkerPosition(
LatLng newPosition, double newHeading, String icon) {
_updateMarkerPosition(newPosition, newHeading, icon);
}
void _smoothlyUpdateMarker(
Marker oldMarker, LatLng newPosition, double newHeading, String icon) {
double distance = Geolocator.distanceBetween(
oldMarker.position.latitude,
oldMarker.position.longitude,
newPosition.latitude,
newPosition.longitude);
if (distance < 2.0) return;
final MarkerId markerIdKey = oldMarker.markerId;
_animationTimers[markerIdKey.value]?.cancel();
int ticks = 0;
const int totalSteps = 20;
const int stepDuration = 50;
double latStep =
(newPosition.latitude - oldMarker.position.latitude) / totalSteps;
double lngStep =
(newPosition.longitude - oldMarker.position.longitude) / totalSteps;
double headingStep = (newHeading - oldMarker.rotation) / totalSteps;
LatLng currentPos = oldMarker.position;
double currentHeading = oldMarker.rotation;
_animationTimers[markerIdKey.value] =
Timer.periodic(const Duration(milliseconds: stepDuration), (timer) {
ticks++;
currentPos =
LatLng(currentPos.latitude + latStep, currentPos.longitude + lngStep);
currentHeading += headingStep;
final updatedMarker = oldMarker.copyWith(
position: currentPos,
rotation: currentHeading,
icon: InlqBitmap.fromStyleImage(icon),
);
markers = {
...markers.where((m) => m.markerId != markerIdKey),
updatedMarker,
};
if (mapController != null) {
mapController!.animateCamera(CameraUpdate.newLatLng(currentPos));
}
update();
if (ticks >= totalSteps) {
timer.cancel();
_animationTimers.remove(markerIdKey.value);
}
});
}
// تحديث موقع العلامة (Marker) واتجاهها بسلاسة على الخريطة.
// تحسب الدالة المسافة بين الموقع الحالي والجديد؛ وإذا كانت أكبر من مترين،
// تقوم بتقسيم الحركة والدوران إلى 20 خطوة متباعدة بـ 50 مللي ثانية (إجمالي ثانية واحدة).
// يتم تحديث موضع العلامة وتحريك الكاميرا تدريجياً لتبدو حركة السيارة انسيابية.
void smoothlyUpdateMarker(
Marker oldMarker, LatLng newPosition, double newHeading, String icon) {
_smoothlyUpdateMarker(oldMarker, newPosition, newHeading, icon);
}
void changeBottomSheetShown({bool? forceValue}) {
if (forceValue != null) {
isBottomSheetShown = forceValue;
} else {
isBottomSheetShown = !isBottomSheetShown;
}
heightBottomSheetShown = isBottomSheetShown == true ? 250 : 0;
update();
}
void changeCashConfirmPageShown() {
isCashConfirmPageShown = !isCashConfirmPageShown;
final rideLife = Get.find<RideLifecycleController>();
rideLife.isCashSelectedBeforeConfirmRide = true;
cashConfirmPageShown = isCashConfirmPageShown == true ? 250 : 0;
update();
rideLife.update();
}
void changePaymentMethodPageShown() {
isPaymentMethodPageShown = !isPaymentMethodPageShown;
paymentPageShown = isPaymentMethodPageShown == true ? Get.height * .6 : 0;
update();
}
void changeMapType() {
mapType = !mapType;
update();
}
void changeMapTraffic() {
mapTrafficON = !mapTrafficON;
update();
}
void changeisAnotherOreder(bool val) {
isAnotherOreder = val;
update();
}
void changeIsWhatsAppOrder(bool val) {
isWhatsAppOrder = val;
update();
}
void changeCancelRidePageShow() {
showCancelRideBottomSheet();
isCancelRidePageShown = !isCancelRidePageShown;
update();
if (Get.isRegistered<RideLifecycleController>()) {
Get.find<RideLifecycleController>().update();
}
}
void getDrawerMenu() {
heightMenuBool = !heightMenuBool;
widthMapTypeAndTraffic = heightMenuBool == true ? 0 : 50;
heightMenu = heightMenuBool == true ? 80 : 0;
widthMenu = heightMenuBool == true ? 110 : 0;
update();
}
void changeMainBottomMenuMap() {
if (isWayPointStopsSheetUtilGetMap == true) {
changeWayPointSheet();
} else {
isMainBottomMenuMap = !isMainBottomMenuMap;
mainBottomMenuMapHeight =
isMainBottomMenuMap == true ? Get.height * .22 : Get.height * .6;
isWayPointSheet = false;
if (heightMenuBool == true) {
getDrawerMenu();
}
Get.find<RideLifecycleController>().initilizeGetStorage();
update();
}
}
void downPoints() {
if (Get.find<WayPointController>().wayPoints.length < 2) {
isWayPointStopsSheetUtilGetMap = false;
isWayPointSheet = false;
wayPointSheetHeight = isWayPointStopsSheet ? Get.height * .45 : 0;
update();
}
update();
}
void changeWayPointSheet() {
isWayPointSheet = !isWayPointSheet;
wayPointSheetHeight = isWayPointSheet == false ? 0 : Get.height * .45;
update();
}
void changeWayPointStopsSheet() {
final locationSearch = Get.find<LocationSearchController>();
if (locationSearch.wayPointIndex > -1) {
isWayPointStopsSheet = true;
isWayPointStopsSheetUtilGetMap = true;
}
isWayPointStopsSheet = !isWayPointStopsSheet;
wayPointSheetHeight = isWayPointStopsSheet ? Get.height * .45 : 0;
update();
}
void changeHeightPlaces() {
final locationSearch = Get.find<LocationSearchController>();
if (locationSearch.placesDestination.isEmpty) {
height = 0;
update();
} else {
height = 150;
update();
}
}
void changeHeightStartPlaces() {
final locationSearch = Get.find<LocationSearchController>();
if (locationSearch.placesStart.isEmpty) {
height = 0;
update();
} else {
height = 150;
update();
}
}
void changeHeightPlacesAll(int index) {
final locationSearch = Get.find<LocationSearchController>();
if (locationSearch.placeListResponseAll[index].isEmpty) {
height = 0;
update();
} else {
height = 150;
update();
}
}
void changeHeightPlaces1() {
final locationSearch = Get.find<LocationSearchController>();
if (locationSearch.wayPoint1.isEmpty) {
height = 0;
update();
} else {
height = 150;
update();
}
}
void changeHeightPlaces2() {
final locationSearch = Get.find<LocationSearchController>();
if (locationSearch.wayPoint2.isEmpty) {
height = 0;
update();
} else {
height = 150;
update();
}
}
void changeHeightPlaces3() {
final locationSearch = Get.find<LocationSearchController>();
if (locationSearch.wayPoint3.isEmpty) {
height = 0;
update();
} else {
height = 150;
update();
}
}
void changeHeightPlaces4() {
final locationSearch = Get.find<LocationSearchController>();
if (locationSearch.wayPoint4.isEmpty) {
height = 0;
update();
} else {
height = 150;
update();
}
}
void hidePlaces() {
height = 0;
update();
}
void changePickerShown() {
isPickerShown = !isPickerShown;
heightPickerContainer = isPickerShown == true ? 150 : 90;
update();
}
void _initializePolygons() {
List<List<LatLng>> universityPolygons =
UniversitiesPolygons.universityPolygons;
for (int i = 0; i < universityPolygons.length; i++) {
Polygon polygon = Polygon(
polygonId: PolygonId('univ_$i'),
points: universityPolygons[i],
fillColor: Colors.blueAccent.withOpacity(0.2),
strokeColor: Colors.blueAccent,
strokeWidth: 2,
);
polygons.add(polygon);
}
update();
}
void _applyLowEndModeIfNeeded() {
// Placeholder comment from original
}
Future<void> _initMinimalIcons() async {
// Icons are loaded dynamically
}
Future<void> _playRouteAnimation(
List<LatLng> coords, LatLngBounds? bounds) async {
const List<Color> segmentColors = [
Color(0xFF109642), // Green
Color(0xFFF59E0B), // Amber
Color(0xFF7C3AED), // Purple
Color(0xFFEF4444), // Red
];
Set<Polyline> newPolylines = {};
final locSearch = Get.find<LocationSearchController>();
if (locSearch.activeMenuWaypointCount > 0) {
List<int> splitIndices = [];
for (int w = 0; w < locSearch.activeMenuWaypointCount; w++) {
final wp = locSearch.menuWaypoints[w];
if (wp == null) continue;
int bestIdx = 0;
double bestDist = double.infinity;
for (int j = 0; j < coords.length; j++) {
final dx = coords[j].latitude - wp.latitude;
final dy = coords[j].longitude - wp.longitude;
final d = dx * dx + dy * dy;
if (d < bestDist) {
bestDist = d;
bestIdx = j;
}
}
splitIndices.add(bestIdx);
}
splitIndices.sort();
List<int> boundaries = [0, ...splitIndices, coords.length - 1];
for (int s = 0; s < boundaries.length - 1; s++) {
int from = boundaries[s];
int to = boundaries[s + 1] + 1;
if (to > coords.length) to = coords.length;
if (from >= to - 1) continue;
final segCoords = coords.sublist(from, to);
if (segCoords.length < 2) continue;
final color = segmentColors[s % segmentColors.length];
newPolylines.add(Polyline(
polylineId: PolylineId('segment_$s'),
points: segCoords,
color: color,
width: 6,
));
}
} else {
newPolylines.add(Polyline(
polylineId: const PolylineId('route_primary'),
points: coords,
color: AppColor.primaryColor,
width: 6,
));
}
polyLines = newPolylines;
update();
Log.print(
'🗺️ Drawing ${markers.length} markers + ${polyLines.length} polylines on map');
update();
if (bounds != null) {
await _safeAnimateCameraBounds(bounds);
}
}
void reset() {
isPickerShown = false;
isPointsPageForRider = false;
isBottomSheetShown = false;
isCancelRidePageShown = false;
isCashConfirmPageShown = false;
isPaymentMethodPageShown = false;
isRideFinished = false;
rideConfirm = false;
isMainBottomMenuMap = true;
isWayPointSheet = false;
isWayPointStopsSheet = false;
isWayPointStopsSheetUtilGetMap = false;
heightBottomSheetShown = 0;
mainBottomMenuMapHeight = Get.height * 0.22;
wayPointSheetHeight = 0;
markers.clear();
polyLines.clear();
polylineCoordinates.clear();
_animationTimers.forEach((key, timer) => timer.cancel());
_animationTimers.clear();
update();
}
@override
void onClose() {
_animationTimers.forEach((key, timer) => timer.cancel());
_animationTimers.clear();
mapController = null;
super.onClose();
}
}

View File

@@ -0,0 +1,25 @@
import 'package:get/get.dart';
import 'map_socket_controller.dart';
import 'map_engine_controller.dart';
import 'location_search_controller.dart';
import 'nearby_drivers_controller.dart';
import 'ride_lifecycle_controller.dart';
import 'ui_interactions_controller.dart';
class MapScreenBinding extends Bindings {
@override
void dependencies() {
// 1. WebSocket Controller: Permanent and immediate
Get.put(MapSocketController());
// 2. Core Controllers (initialized when the screen opens or on demand)
Get.lazyPut(() => MapEngineController());
Get.lazyPut(() => LocationSearchController());
Get.lazyPut(() => NearbyDriversController());
// 3. Lifecycle and UI Interaction Controllers
Get.lazyPut(() => RideLifecycleController());
Get.lazyPut(() => UiInteractionsController(), fenix: true);
}
}

View File

@@ -0,0 +1,326 @@
import 'dart:async';
import 'dart:convert';
import 'package:get/get.dart';
import 'package:socket_io_client/socket_io_client.dart' as io_client;
import 'package:intaleq_maps/intaleq_maps.dart';
import '../../../constant/box_name.dart';
import '../../../constant/links.dart';
import '../../../main.dart'; // contains global 'box'
import '../../../print.dart';
import 'ride_lifecycle_controller.dart';
import 'nearby_drivers_controller.dart';
import 'map_engine_controller.dart';
class MapSocketController extends GetxController {
late io_client.Socket socket;
bool isSocketConnected = false;
bool _isSocketInitialized = false;
Timer? _heartbeatTimer;
DateTime? _lastSocketLocationTime;
int _socketLocationUpdatesCount = 0;
Timer? _watchdogTimer;
DateTime? get lastDriverLocationTime => _lastSocketLocationTime;
int get socketLocationUpdatesCount => _socketLocationUpdatesCount;
void initConnectionWithSocket() {
if (isSocketConnected) return;
String passengerId = box.read(BoxName.passengerID).toString();
Log.print("🔌 Initializing Socket for Passenger: $passengerId");
socket = io_client.io(
AppLink.serverSocket,
io_client.OptionBuilder()
.setTransports(['websocket'])
.disableAutoConnect()
.setQuery({'id': passengerId})
.setReconnectionAttempts(20)
.setReconnectionDelay(2000)
.setReconnectionDelayMax(10000)
.enableReconnection()
.setTimeout(20000)
.setExtraHeaders({'Connection': 'Upgrade'})
.build(),
);
_isSocketInitialized = true;
socket.connect();
socket.onConnect((_) {
Log.print("✅ Socket Connected Successfully");
isSocketConnected = true;
_startHeartbeat();
final rideLifecycle = Get.find<RideLifecycleController>();
if (rideLifecycle.rideId != 'yet' && rideLifecycle.driverId.isNotEmpty) {
socket.emit('subscribe_driver_location', {
'ride_id': rideLifecycle.rideId,
'driver_id': rideLifecycle.driverId,
});
Log.print("📡 Re-subscribed to driver location after connect");
}
update();
});
socket.onDisconnect((_) {
Log.print("⚠️ Socket Disconnected — Auto-Reconnect will handle it");
isSocketConnected = false;
final rideLifecycle = Get.find<RideLifecycleController>();
if (rideLifecycle.isActiveRideState()) {
Log.print("🔄 Enabling Fast Polling Fallback (4s) until reconnect...");
rideLifecycle.startMasterTimerWithInterval(4);
}
update();
});
socket.onReconnect((_) {
Log.print("🔁 Socket Reconnected Successfully!");
isSocketConnected = true;
_startHeartbeat();
final rideLifecycle = Get.find<RideLifecycleController>();
if (rideLifecycle.rideId != 'yet' && rideLifecycle.driverId.isNotEmpty) {
socket.emit('subscribe_driver_location', {
'ride_id': rideLifecycle.rideId,
'driver_id': rideLifecycle.driverId,
});
Log.print("📡 Re-subscribed to driver location after reconnect");
}
if (rideLifecycle.isActiveRideState()) {
Log.print("✅ Socket back online — stopping Fast Polling Fallback");
rideLifecycle.cancelMasterTimer();
}
update();
});
socket.onReconnectAttempt((attemptNumber) {
Log.print("🔄 Socket Reconnect Attempt #$attemptNumber...");
});
socket.onError((error) {
Log.print("❌ Socket Error: $error");
isSocketConnected = false;
});
socket.on('connect_error', (error) {
Log.print("❌ Socket Connect Error: $error");
isSocketConnected = false;
// في الإصدار 1.0.2 أحياناً auto-reconnect لا يعمل بعد connect_error
// نتأكد يدوياً من إعادة الاتصال
Future.delayed(const Duration(seconds: 3), () {
if (!isSocketConnected && _isSocketInitialized) {
Log.print("🔄 Manual reconnect after connect_error...");
try {
socket.connect();
} catch (e) {
Log.print("Manual reconnect error: $e");
}
}
});
});
socket.on('ride_status_change', (data) {
Log.print("📩 Socket Event: ride_status_change -> $data");
_handleRideStatusChangeWithSocket(data);
});
socket.on('driver_location_update', (data) {
handleDriverLocationUpdate(data);
});
}
void _startHeartbeat() {
_heartbeatTimer?.cancel();
_heartbeatTimer = Timer.periodic(const Duration(seconds: 15), (timer) {
if (isSocketConnected && socket.connected) {
socket.emit('heartbeat',
{'passenger_id': box.read(BoxName.passengerID).toString()});
}
});
}
bool isSocketHealthy() {
if (!isSocketConnected) return false;
if (_lastSocketLocationTime == null) return false;
final diff = DateTime.now().difference(_lastSocketLocationTime!).inSeconds;
return diff < 20;
}
void _handleRideStatusChangeWithSocket(dynamic data) {
if (data == null || data['status'] == null) return;
String newStatus = data['status'].toString().toLowerCase();
Log.print("🔔 Socket Status Update: $newStatus");
final rideLifecycle = Get.find<RideLifecycleController>();
Map<String, dynamic>? driverInfo;
if (data['driver_info'] != null && data['driver_info'] is Map) {
driverInfo = Map<String, dynamic>.from(data['driver_info']);
}
switch (newStatus) {
case 'accepted':
case 'apply':
case 'applied':
rideLifecycle.processRideAcceptance(
driverData: driverInfo, source: "Socket");
break;
case 'arrived':
rideLifecycle.processDriverArrival("Socket");
break;
case 'started':
case 'begin':
rideLifecycle.processRideBegin(source: "Socket");
break;
case 'finished':
case 'ended':
_onRideFinishedWithSocket(data);
break;
case 'cancelled':
rideLifecycle.processRideCancelledByDriver(data, source: "Socket");
break;
case 'no_drivers_found':
rideLifecycle.showNoDriverDialog();
break;
}
}
void _onRideFinishedWithSocket(dynamic data) {
Log.print("🏁 Ride Finished (Socket)");
final rideLifecycle = Get.find<RideLifecycleController>();
var rawList = data['DriverList'];
List<dynamic> listToSend = [];
if (rawList != null) {
if (rawList is List) {
listToSend = rawList;
} else if (rawList is String) {
try {
listToSend = jsonDecode(rawList);
} catch (e) {
Log.print("Error decoding DriverList: $e");
}
}
}
if (listToSend.isEmpty && data['price'] != null) {
listToSend = [
rideLifecycle.driverId,
rideLifecycle.rideId,
rideLifecycle.driverToken,
data['price'].toString()
];
}
rideLifecycle.processRideFinished(listToSend, source: "Socket");
}
void handleDriverLocationUpdate(dynamic data) {
if (!isSocketConnected || data == null) return;
_lastSocketLocationTime = DateTime.now();
_socketLocationUpdatesCount++;
final rideLifecycle = Get.find<RideLifecycleController>();
if (rideLifecycle.driverId.isEmpty &&
(data['driver_id'] ?? data['driverId']) != null) {
rideLifecycle.driverId =
(data['driver_id'] ?? data['driverId']).toString();
}
if (_socketLocationUpdatesCount >= 3 &&
rideLifecycle.locationPollingTimer != null) {
Log.print("✅ Socket delivering locations reliably. Stopping polling.");
rideLifecycle.stopDriverLocationPolling();
}
try {
double lat = double.tryParse(
(data['latitude'] ?? data['lat'])?.toString() ?? '0') ??
0;
double lng = double.tryParse(
(data['longitude'] ?? data['lng'])?.toString() ?? '0') ??
0;
double heading = double.tryParse(data['heading']?.toString() ?? '0') ?? 0;
if (lat == 0 || lng == 0) return;
LatLng newPos = LatLng(lat, lng);
final nearbyDrivers = Get.find<NearbyDriversController>();
if (nearbyDrivers.driverCarsLocationToPassengerAfterApplied.isEmpty) {
nearbyDrivers.driverCarsLocationToPassengerAfterApplied.add(newPos);
} else {
nearbyDrivers.driverCarsLocationToPassengerAfterApplied[0] = newPos;
}
double speed = double.tryParse(data['speed']?.toString() ?? '0') ?? 0;
rideLifecycle.checkAndRecalculateIfDeviated(
newPos,
heading: heading,
speed: speed,
);
final mapEngine = Get.find<MapEngineController>();
if (mapEngine.mapController != null) {
double zoom = 16.5;
if (speed > 0) {
zoom = 17.0 - ((speed - 10) / 70) * 2.5;
zoom = zoom.clamp(14.5, 17.0);
}
mapEngine.mapController!
.animateCamera(CameraUpdate.newLatLngZoom(newPos, zoom));
}
final dynamic distanceValue =
data['distance_m'] ?? data['distance_meters'] ?? data['distance'];
final double? distanceMeters =
double.tryParse(distanceValue?.toString() ?? '');
final int? etaSeconds = data['eta_seconds'] == null
? null
: int.tryParse(data['eta_seconds'].toString());
final bool hasServerMetrics = (etaSeconds != null && etaSeconds > 0) ||
(distanceMeters != null && distanceMeters > 0);
if (hasServerMetrics) {
rideLifecycle.updateDriverRouteMetrics(
etaSeconds: etaSeconds != null && etaSeconds > 0 ? etaSeconds : null,
distanceMeters: distanceMeters,
);
}
rideLifecycle.updateDriverMarker(newPos, heading);
rideLifecycle.updateRemainingRoute(newPos, updateEta: !hasServerMetrics);
rideLifecycle.update();
} catch (e) {
Log.print('Error in handleDriverLocationUpdate: $e');
}
}
void disposeRideSocket() {
_heartbeatTimer?.cancel();
_watchdogTimer?.cancel();
if (_isSocketInitialized) {
socket.disconnect();
socket.dispose();
isSocketConnected = false;
_isSocketInitialized = false;
Log.print("🔌 Socket Disposed");
}
}
@override
void onClose() {
disposeRideSocket();
super.onClose();
}
}

View File

@@ -0,0 +1,475 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math' show Random, atan2, cos, pi, pow, sin, sqrt;
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import '../../../constant/links.dart';
import '../../../constant/api_key.dart';
import '../../../print.dart';
import '../../functions/crud.dart';
import 'map_engine_controller.dart';
import 'location_search_controller.dart';
import 'ride_lifecycle_controller.dart';
import '../../../models/model/locations.dart';
import 'car_location.dart';
import 'package:device_info_plus/device_info_plus.dart';
class NearbyDriversController extends GetxController {
List carsLocationByPassenger = [];
List<LatLng> driverCarsLocationToPassengerAfterApplied = [];
List<CarLocationModel> carLocationsModels = [];
String? currentDriverMarkerId;
bool lowPerf = false;
dynamic dataCarsLocationByPassenger;
bool noCarString = false;
final double minMovementThreshold = 2.0;
final Map<String, Timer> _animationTimers = {};
final List<Map<String, dynamic>> fakeCarData = [];
Future<bool> getCarsLocationByPassengerAndReloadMarker() async {
carsLocationByPassenger = [];
final locSearch = Get.find<LocationSearchController>();
if (locSearch.passengerLocation.latitude == 0 && locSearch.passengerLocation.longitude == 0) {
return false;
}
var res = await CRUD().get(
link: AppLink.getCarsLocationByPassenger,
payload: {
'lat': locSearch.passengerLocation.latitude.toString(),
'lng': locSearch.passengerLocation.longitude.toString(),
'radius': '5',
'limit': '50',
},
);
if (res == 'failure') {
noCarString = true;
dataCarsLocationByPassenger = 'failure';
update();
return false;
}
noCarString = false;
var responseData = jsonDecode(res);
dataCarsLocationByPassenger = responseData;
List driversList = [];
if (responseData['status'] == true && responseData['data'] != null) {
driversList = responseData['data'];
} else if (responseData['message'] != null) {
driversList = responseData['message'];
}
final mapEngine = Get.find<MapEngineController>();
if (driversList.isEmpty) {
carsLocationByPassenger.clear();
mapEngine.update();
return false;
}
carsLocationByPassenger.clear();
for (var i = 0; i < driversList.length; i++) {
var carData = driversList[i];
double lat = double.tryParse(carData['latitude'].toString()) ?? 0.0;
double lng = double.tryParse(carData['longitude'].toString()) ?? 0.0;
double heading = double.tryParse(carData['heading'].toString()) ?? 0.0;
if (lat == 0.0 || lng == 0.0) continue;
String driverId = (carData['driver_id'] ?? carData['id'] ?? '').toString();
if (driverId.isEmpty || driverId == 'null') continue;
_updateOrCreateMarker(
driverId,
LatLng(lat, lng),
heading,
_getIconForCar(carData),
);
}
mapEngine.update();
return true;
}
void _addFakeCarMarkers(LatLng center, int count) {
if (fakeCarData.isEmpty) {
Random random = Random();
double radiusInKm = 2.5;
for (int i = 0; i < count; i++) {
double angle = random.nextDouble() * 2 * pi;
double distance = sqrt(random.nextDouble()) * radiusInKm;
double latOffset = (distance / 111.32);
double lonOffset =
(distance / (111.32 * cos(center.latitude * pi / 180.0)));
double lat = center.latitude + (latOffset * cos(angle));
double lon = center.longitude + (lonOffset * sin(angle));
double heading = random.nextDouble() * 360;
fakeCarData.add({
'id': 'fake_$i',
'latitude': lat,
'longitude': lon,
'heading': heading,
'gender': 'Male',
});
}
}
for (var carData in fakeCarData) {
_updateOrCreateMarker(
carData['id'].toString(),
LatLng(carData['latitude'], carData['longitude']),
carData['heading'],
_getIconForCar(carData),
);
}
}
void addFakeCarMarkers(LatLng center, int count) {
_addFakeCarMarkers(center, count);
}
Future<CarLocation?> getNearestDriverByPassengerLocation() async {
final rideLife = Get.find<RideLifecycleController>();
final locSearch = Get.find<LocationSearchController>();
if (!rideLife.rideConfirm) {
if (dataCarsLocationByPassenger != 'failure' &&
dataCarsLocationByPassenger != null &&
dataCarsLocationByPassenger.containsKey('message') &&
dataCarsLocationByPassenger['message'] != null &&
dataCarsLocationByPassenger['message'].length > 0) {
double nearestDistance = double.infinity;
CarLocation? nearestCar;
for (var i = 0;
i < dataCarsLocationByPassenger['message'].length;
i++) {
var carLocation = dataCarsLocationByPassenger['message'][i];
try {
final distance = Geolocator.distanceBetween(
locSearch.passengerLocation.latitude,
locSearch.passengerLocation.longitude,
double.parse(carLocation['latitude']),
double.parse(carLocation['longitude']),
);
int durationToPassenger = (distance / 1000 / 25 * 3600).round();
update();
if (distance < nearestDistance) {
nearestDistance = distance;
nearestCar = CarLocation(
distance: distance,
duration: durationToPassenger.toDouble(),
id: carLocation['driver_id'],
latitude: double.parse(carLocation['latitude']),
longitude: double.parse(carLocation['longitude']),
);
update();
}
} catch (e) {
Log.print('Error calculating distance/duration: $e');
}
}
return nearestCar;
}
}
return null;
}
Future<CarLocation?> getNearestDriverByPassengerLocationAPIGOOGLE() async {
final rideLife = Get.find<RideLifecycleController>();
final mapEngine = Get.find<MapEngineController>();
final locSearch = Get.find<LocationSearchController>();
if (mapEngine.polyLines.isEmpty || rideLife.totalCostPassenger == 0) {
return null;
}
if (!rideLife.rideConfirm) {
double nearestDistance = double.infinity;
if (dataCarsLocationByPassenger != 'failure' &&
dataCarsLocationByPassenger != null &&
dataCarsLocationByPassenger.containsKey('message') &&
dataCarsLocationByPassenger['message'] != null) {
if (dataCarsLocationByPassenger['message'].length > 0) {
CarLocation? nearestCar;
for (var i = 0;
i < dataCarsLocationByPassenger['message'].length;
i++) {
var carLocation = dataCarsLocationByPassenger['message'][i];
update();
String apiUrl =
'${AppLink.googleMapsLink}distancematrix/json?destinations=${carLocation['latitude']},${carLocation['longitude']}&origins=${locSearch.passengerLocation.latitude},${locSearch.passengerLocation.longitude}&units=metric&key=${AK.mapAPIKEY}';
var response = await CRUD().getGoogleApi(link: apiUrl, payload: {});
if (response != null && response['status'] == "OK") {
var data = response;
int distance1 =
data['rows'][0]['elements'][0]['distance']['value'];
rideLife.distanceByPassenger =
data['rows'][0]['elements'][0]['distance']['text'];
rideLife.durationToPassenger =
data['rows'][0]['elements'][0]['duration']['value'];
Duration durationFromDriverToPassenger =
Duration(seconds: rideLife.durationToPassenger.toInt());
rideLife.stringRemainingTimeToPassenger =
data['rows'][0]['elements'][0]['duration']['text'];
update();
if (distance1 < nearestDistance) {
nearestDistance = distance1.toDouble();
nearestCar = CarLocation(
distance: distance1.toDouble(),
duration: rideLife.durationToPassenger.toDouble(),
id: carLocation['driver_id'],
latitude: double.parse(carLocation['latitude']),
longitude: double.parse(carLocation['longitude']),
);
update();
}
} else {
Log.print('${response?['status']}: error Google distance matrix');
}
}
return nearestCar;
}
}
}
return null;
}
Future getCarForFirstConfirm(String carType) async {
bool foundCars = false;
int attempt = 0;
Timer.periodic(const Duration(seconds: 4), (Timer t) async {
foundCars = await getCarsLocationByPassengerAndReloadMarker();
Log.print('foundCars: $foundCars');
if (foundCars) {
t.cancel();
} else if (attempt >= 4) {
t.cancel();
if (!foundCars) {
noCarString = true;
dataCarsLocationByPassenger = 'failure';
}
update();
}
attempt++;
});
}
void startCarLocationSearch(String carType) {
int searchInterval = 5;
Log.print('searchInterval: $searchInterval');
int boundIncreaseStep = 2500;
Log.print('boundIncreaseStep: $boundIncreaseStep');
int maxAttempts = 3;
int maxBoundIncreaseStep = 6000;
int attempt = 0;
Log.print('initial attempt: $attempt');
Timer.periodic(Duration(seconds: searchInterval), (Timer timer) async {
Log.print('Current attempt: $attempt');
bool foundCars = false;
final mapEngine = Get.find<MapEngineController>();
if (attempt >= maxAttempts) {
timer.cancel();
if (foundCars == false) {
noCarString = true;
update();
}
} else if (mapEngine.reloadStartApp == true) {
Log.print('reloadStartApp: ${mapEngine.reloadStartApp}');
foundCars = await getCarsLocationByPassengerAndReloadMarker();
Log.print('foundCars: $foundCars');
if (foundCars) {
timer.cancel();
} else {
attempt++;
Log.print('Incrementing attempt to: $attempt');
if (boundIncreaseStep < maxBoundIncreaseStep) {
boundIncreaseStep += 1500;
if (boundIncreaseStep > maxBoundIncreaseStep) {
boundIncreaseStep = maxBoundIncreaseStep;
}
Log.print('New boundIncreaseStep: $boundIncreaseStep');
}
}
}
});
}
String _getIconForCar(Map<String, dynamic> carData) {
final mapEngine = Get.find<MapEngineController>();
if (carData['model'].toString().contains('دراجة')) {
return mapEngine.motoIcon;
} else if (carData['gender'] == 'Female') {
return mapEngine.ladyIcon;
} else {
return mapEngine.carIcon;
}
}
void _updateOrCreateMarker(
String markerId, LatLng newPosition, double newHeading, String icon) {
final mapEngine = Get.find<MapEngineController>();
if (!mapEngine.isIconsLoaded) {
Log.print("⚠️ Skipping drawing marker $markerId because map icons are not fully loaded yet.");
return;
}
final mId = MarkerId(markerId);
final existingMarker = mapEngine.markers.cast<Marker?>().firstWhere(
(m) => m?.markerId == mId,
orElse: () => null,
);
if (existingMarker == null) {
mapEngine.markers = {
...mapEngine.markers,
Marker(
markerId: mId,
position: newPosition,
rotation: newHeading,
icon: InlqBitmap.fromStyleImage(icon),
anchor: const Offset(0.5, 0.5),
),
};
mapEngine.update();
} else {
double distance = Geolocator.distanceBetween(
existingMarker.position.latitude,
existingMarker.position.longitude,
newPosition.latitude,
newPosition.longitude);
if (distance >= minMovementThreshold) {
_smoothlyUpdateMarker(existingMarker, newPosition, newHeading, icon);
}
}
}
void _smoothlyUpdateMarker(
Marker oldMarker, LatLng newPosition, double newHeading, String icon) {
final mapEngine = Get.find<MapEngineController>();
final MarkerId markerIdKey = oldMarker.markerId;
_animationTimers[markerIdKey.value]?.cancel();
int ticks = 0;
const int totalSteps = 20;
const int stepDuration = 50;
double latStep =
(newPosition.latitude - oldMarker.position.latitude) / totalSteps;
double lngStep =
(newPosition.longitude - oldMarker.position.longitude) / totalSteps;
double headingStep = (newHeading - oldMarker.rotation) / totalSteps;
LatLng currentPos = oldMarker.position;
double currentHeading = oldMarker.rotation;
_animationTimers[markerIdKey.value] =
Timer.periodic(const Duration(milliseconds: stepDuration), (timer) {
ticks++;
currentPos =
LatLng(currentPos.latitude + latStep, currentPos.longitude + lngStep);
currentHeading += headingStep;
final updatedMarker = oldMarker.copyWith(
position: currentPos,
rotation: currentHeading,
icon: InlqBitmap.fromStyleImage(icon),
);
mapEngine.markers = {
...mapEngine.markers.where((m) => m.markerId != markerIdKey),
updatedMarker,
};
if (mapEngine.mapController != null) {
mapEngine.mapController!.animateCamera(CameraUpdate.newLatLng(currentPos));
}
mapEngine.update();
if (ticks >= totalSteps) {
timer.cancel();
_animationTimers.remove(markerIdKey.value);
}
});
}
double calculateBearing(double lat1, double lon1, double lat2, double lon2) {
double deltaLon = lon2 - lon1;
double y = sin(deltaLon) * cos(lat2);
double x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(deltaLon);
double bearing = atan2(y, x);
return (bearing * 180 / pi + 360) % 360;
}
void analyzeBehavior(Position currentPosition, List<LatLng> routePoints) {
double actualBearing = currentPosition.heading;
double expectedBearing = calculateBearing(
routePoints[0].latitude,
routePoints[0].longitude,
routePoints[1].latitude,
routePoints[1].longitude,
);
double bearingDifference = (expectedBearing - actualBearing).abs();
if (bearingDifference > 30) {
Log.print("⚠️ السائق انحرف عن المسار!");
}
}
void detectStops(Position currentPosition) {
if (currentPosition.speed < 0.5) {
Log.print("🚦 السائق توقف في موقع غير متوقع!");
}
}
Future<void> detectPerfMode() async {
try {
if (GetPlatform.isAndroid) {
final info = await DeviceInfoPlugin().androidInfo;
final sdk = info.version.sdkInt;
final ram = info.availableRamSize;
lowPerf = (sdk < 28) || (ram > 0 && ram < 3 * 1024 * 1024 * 1024);
} else {
lowPerf = false;
}
} catch (_) {
lowPerf = false;
}
update();
}
@override
void onClose() {
_animationTimers.forEach((key, timer) => timer.cancel());
_animationTimers.clear();
super.onClose();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
enum RideState {
noRide, // لا يوجد رحلة جارية، عرض واجهة البحث
cancelled, // تم إلغاء الرحلة
preCheckReview, // يوجد رحلة منتهية، تحقق من التقييم
searching, // جاري البحث عن كابتن
driverApplied, // تم قبول الطلب
driverArrived, // وصل السائق
inProgress, // الرحلة بدأت بالفعل
finished, // انتهت الرحلة (سيتم تحويلها إلى preCheckReview)
}

View File

@@ -0,0 +1,436 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import '../../../constant/box_name.dart';
import '../../../constant/colors.dart';
import '../../../constant/links.dart';
import '../../../constant/style.dart';
import '../../../constant/info.dart';
import '../../../main.dart'; // contains global 'box'
import '../../../print.dart';
import '../../../services/emergency_signal_service.dart';
import '../../../views/widgets/elevated_btn.dart';
import '../../../views/widgets/mydialoug.dart';
import '../../../views/widgets/my_textField.dart';
import '../../../views/home/map_page_passenger.dart';
import '../../../views/widgets/error_snakbar.dart';
import '../../../models/model/painter_copoun.dart';
import '../../functions/launch.dart';
import '../../firebase/local_notification.dart';
import '../../firebase/notification_service.dart';
import '../../functions/crud.dart';
import '../../functions/tts.dart';
import 'ride_lifecycle_controller.dart';
import 'location_search_controller.dart';
import 'map_engine_controller.dart';
class UiInteractionsController extends GetxController {
TextEditingController sosPhonePassengerProfile = TextEditingController();
TextEditingController whatsAppLocationText = TextEditingController();
final sosFormKey = GlobalKey<FormState>();
@override
void onInit() {
super.onInit();
EmergencySignalService.instance.startListening(() {
final rideLifecycle = Get.find<RideLifecycleController>();
if (rideLifecycle.statusRide == 'Begin' ||
rideLifecycle.statusRide == 'start') {
Log.print("🚨 Emergency shake verified! Prompting SOS...");
sosPassenger();
}
});
}
Future<void> _ensureSosNumber(Function onSuccess) async {
String? storedPhone = box.read(BoxName.sosPhonePassenger);
if (storedPhone != null && storedPhone.isNotEmpty) {
onSuccess();
return;
}
sosPhonePassengerProfile.clear();
Get.defaultDialog(
title: 'Add SOS Phone'.tr,
titleStyle: AppStyle.title,
content: Form(
key: sosFormKey,
child: Column(
children: [
MyTextForm(
controller: sosPhonePassengerProfile,
label: 'insert sos phone'.tr,
hint: 'e.g. 0912345678 (Default +963)'.tr,
type: TextInputType.phone,
),
const SizedBox(height: 10),
Text(
"Note: If no country code is entered, it will be saved as Syrian (+963).".tr,
style: TextStyle(fontSize: 12, color: Colors.grey),
textAlign: TextAlign.center,
),
],
),
),
confirm: MyElevatedButton(
title: 'Save'.tr,
onPressed: () async {
if (sosFormKey.currentState!.validate()) {
Get.back();
var numberPhone =
formatSyrianPhoneNumber(sosPhonePassengerProfile.text);
await CRUD().post(
link: AppLink.updateprofile,
payload: {
'id': box.read(BoxName.passengerID),
'sosPhone': numberPhone,
},
);
box.write(BoxName.sosPhonePassenger, numberPhone);
onSuccess();
}
},
),
cancel: MyElevatedButton(
title: 'Cancel'.tr,
onPressed: () => Get.back(),
kolor: AppColor.redColor,
)
);
}
void sosPassenger() {
_ensureSosNumber(() {
Get.defaultDialog(
barrierDismissible: false,
title: "Emergency SOS".tr,
titleStyle: AppStyle.title.copyWith(color: AppColor.redColor),
content: Column(
children: [
Icon(Icons.warning_amber_rounded, size: 50, color: AppColor.redColor),
const SizedBox(height: 10),
Text(
"Do you want to send an emergency message to your SOS contact?".tr,
textAlign: TextAlign.center,
style: AppStyle.title,
),
],
),
confirm: MyElevatedButton(
title: "Send SOS".tr,
kolor: AppColor.redColor,
onPressed: () {
Get.back();
_shareTripDetailsSOS();
},
),
cancel: MyElevatedButton(
title: "I'm Safe".tr,
kolor: AppColor.greenColor,
onPressed: () {
Get.back();
},
),
);
});
}
void _shareTripDetailsSOS() {
final rideLifecycle = Get.find<RideLifecycleController>();
final locSearch = Get.find<LocationSearchController>();
String message = "**Emergency SOS from Passenger:**\n";
String origin = locSearch.startNameAddress;
String destination = locSearch.endNameAddress;
message += "* ${'Origin'.tr}: $origin\n";
message += "* ${'Destination'.tr}: $destination\n";
message += "* ${'Driver Name'.tr}: ${rideLifecycle.driverName}\n";
message +=
"* ${'Car'.tr}: ${rideLifecycle.make} - ${rideLifecycle.model} - ${rideLifecycle.licensePlate}\n";
message += "* ${'Phone'.tr}: ${rideLifecycle.driverPhone}\n\n";
message +=
"${'Location'.tr}: https://www.google.com/maps/search/?api=1&query=${locSearch.passengerLocation.latitude},${locSearch.passengerLocation.longitude}\n";
message += "Please help! Contact me as soon as possible.".tr;
launchCommunication(
'whatsapp', box.read(BoxName.sosPhonePassenger), message);
}
String formatSyrianPhone(String phone) {
phone = phone.replaceAll(' ', '').replaceAll('+', '');
if (phone.startsWith('00963')) {
phone = phone.replaceFirst('00963', '963');
}
if (phone.startsWith('0963')) {
phone = phone.replaceFirst('0963', '963');
}
if (phone.startsWith('963')) {
return phone;
}
if (phone.startsWith('09')) {
return '963' + phone.substring(1);
}
if (phone.startsWith('9') && phone.length == 9) {
return '963' + phone;
}
return phone;
}
String formatSyrianPhoneNumber(String phoneNumber) {
String trimmedPhone = phoneNumber.trim();
if (trimmedPhone.startsWith('09')) {
return '963${trimmedPhone.substring(1)}';
}
if (trimmedPhone.startsWith('963')) {
return trimmedPhone;
}
return '963$trimmedPhone';
}
void sendSMS(String to) async {
final rideLifecycle = Get.find<RideLifecycleController>();
String formattedDriverPhone =
rideLifecycle.driverPhone.replaceAll(' ', '').replaceAll('+', '');
String message =
'Hi! This is ${(box.read(BoxName.name).toString().split(' ')[0]).toString()}.\n I am using ${box.read(AppInformation.appName)} to ride with ${rideLifecycle.passengerName} as the driver. ${rideLifecycle.passengerName} \nis driving a ${rideLifecycle.model}\n with license plate ${rideLifecycle.licensePlate}.\n I am currently located at ${Get.find<LocationSearchController>().passengerLocation}.\n If you need to reach me, please contact the driver directly at\n\n $formattedDriverPhone.';
launchCommunication('sms', to, message);
}
void sendWhatsapp(String to) async {
final rideLifecycle = Get.find<RideLifecycleController>();
final locSearch = Get.find<LocationSearchController>();
String formattedPhone = formatSyrianPhone(to);
String message =
'${'${'Hi! This is'.tr} ${(box.read(BoxName.name).toString().split(' ')[0]).toString()}.\n${' I am using'.tr}'} ${AppInformation.appName}${' to ride with'.tr} ${rideLifecycle.passengerName}${' as the driver.'.tr} ${rideLifecycle.passengerName} \n${'is driving a '.tr}${rideLifecycle.model}\n${' with license plate '.tr}${rideLifecycle.licensePlate}.\n${' I am currently located at '.tr} https://www.google.com/maps/place/${locSearch.passengerLocation.latitude},${locSearch.passengerLocation.longitude}.\n${' If you need to reach me, please contact the driver directly at'.tr}\n\n ${rideLifecycle.driverPhone}.';
launchCommunication('whatsapp', formattedPhone, message);
}
Future<dynamic> driverArrivePassengerDialoge() {
final rideLifecycle = Get.find<RideLifecycleController>();
return Get.defaultDialog(
barrierDismissible: false,
title: 'Hi ,I Arrive your location'.tr,
titleStyle: AppStyle.title,
middleText: 'Please go to Car Driver'.tr,
middleTextStyle: AppStyle.title,
confirm: MyElevatedButton(
title: 'Ok I will go now.'.tr,
onPressed: () {
NotificationService.sendNotification(
target: rideLifecycle.driverToken.toString(),
title: 'Hi ,I will go now'.tr,
body: 'I will go now'.tr,
isTopic: false,
tone: 'ding',
driverList: [],
category: 'Hi ,I will go now',
);
Get.back();
rideLifecycle.remainingTime = 0;
rideLifecycle.update();
},
),
);
}
void getDialog(String title, String? midTitle, VoidCallback onPressed) {
final textToSpeechController = Get.find<TextToSpeechController>();
Get.defaultDialog(
title: title,
titleStyle: AppStyle.title,
middleTextStyle: AppStyle.title,
content: Column(
children: [
IconButton(
onPressed: () async {
await textToSpeechController.speakText(title ?? midTitle!);
},
icon: const Icon(Icons.headphones),
),
Text(
midTitle!,
style: AppStyle.title,
)
],
),
confirm: MyElevatedButton(
title: 'Ok'.tr,
onPressed: onPressed,
kolor: AppColor.greenColor,
),
cancel: MyElevatedButton(
title: 'Cancel',
kolor: AppColor.redColor,
onPressed: () {
Get.back();
},
),
);
}
Future shareTripWithFamily() async {
_ensureSosNumber(() {
final rideLifecycle = Get.find<RideLifecycleController>();
String storedPhone = box.read(BoxName.sosPhonePassenger)!;
if (rideLifecycle.rideId == 'yet' || rideLifecycle.driverId.isEmpty) {
Get.snackbar("Alert".tr, "Wait for the trip to start first".tr);
return;
}
var numberPhone = formatSyrianPhoneNumber(storedPhone);
String trackingLink = rideLifecycle.generateTrackingLink(
rideLifecycle.rideId, rideLifecycle.driverId);
String message = """
مرحباً، تابع رحلتي مباشرة على تطبيق انطلق 🚗
يمكنك تتبع مسار الرحلة من هنا:
$trackingLink
السائق: ${rideLifecycle.passengerName}
السيارة: ${rideLifecycle.model} - ${rideLifecycle.licensePlate}
شكراً لاستخدامك انطلق!
"""
.tr;
String messageEn = """Hello, follow my trip live on Intaleq 🚗
Track my ride here:
$trackingLink
Driver: ${rideLifecycle.passengerName}
Car: ${rideLifecycle.model} - ${rideLifecycle.licensePlate}
Thank you for using Intaleq!
""";
String userLanguage = box.read(BoxName.lang) ?? 'ar';
message = (userLanguage == 'ar') ? message : messageEn;
Log.print("Sending WhatsApp to: $numberPhone");
launchCommunication('whatsapp', numberPhone, message);
box.write(BoxName.parentTripSelected, true);
update();
});
}
Future getTokenForParent() async {
_ensureSosNumber(() async {
String storedPhone = box.read(BoxName.sosPhonePassenger)!;
var numberPhone = formatSyrianPhoneNumber(storedPhone);
Log.print("Searching for Parent Token with Phone: $numberPhone");
var res = await CRUD()
.post(link: AppLink.getTokenParent, payload: {'phone': numberPhone});
if (res is Map<String, dynamic>) {
handleResponse(res);
} else {
try {
var decoded = jsonDecode(res);
handleResponse(decoded);
} catch (e) {
Log.print("Error parsing parent response: $res");
}
}
});
}
void handleResponse(Map<String, dynamic> res) {
final rideLifecycle = Get.find<RideLifecycleController>();
if (res['status'] == 'failure') {
if (Get.isDialogOpen ?? false) Get.back();
Get.defaultDialog(
title: "No user found".tr,
titleStyle: AppStyle.title,
content: Column(
children: [
Text(
"No passenger found for the given phone number".tr,
style: AppStyle.title,
textAlign: TextAlign.center,
),
const SizedBox(height: 10),
Text(
"Send Intaleq app to him".tr,
style: AppStyle.title
.copyWith(color: AppColor.greenColor, fontSize: 14),
textAlign: TextAlign.center,
)
],
),
confirm: MyElevatedButton(
title: 'Send Invite'.tr,
onPressed: () {
Get.back();
var rawPhone = box.read(BoxName.sosPhonePassenger);
if (rawPhone == null) return;
var phone = formatSyrianPhoneNumber(rawPhone);
var message = '''Dear Friend,
🚀 I have just started an exciting trip on Intaleq!
Download the app to track my ride:
👉 Android: https://play.google.com/store/apps/details?id=com.Intaleq.intaleq&hl=en-US
👉 iOS: https://apps.apple.com/st/app/intaleq-rider/id6748075179
See you there!
Intaleq Team''';
launchCommunication('whatsapp', phone, message);
},
),
cancel: MyElevatedButton(
title: 'Cancel'.tr,
onPressed: () {
Get.back();
},
),
);
} else if (res['status'] == 'success') {
if (Get.isDialogOpen ?? false) Get.back();
Get.snackbar("Success".tr, "The invitation was sent successfully".tr,
backgroundColor: AppColor.greenColor, colorText: Colors.white);
List tokensData = res['data'];
for (var device in tokensData) {
String tokenParent = device['token'];
NotificationService.sendNotification(
category: "Trip Monitoring",
target: tokenParent,
title: "Trip Monitoring".tr,
body: "Click to track the trip".tr,
isTopic: false,
tone: 'tone1',
driverList: [rideLifecycle.rideId, rideLifecycle.driverId],
);
box.write(BoxName.tokenParent, tokenParent);
}
box.write(BoxName.parentTripSelected, true);
}
}
@override
void onClose() {
EmergencySignalService.instance.stopListening();
super.onClose();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
import 'package:get/get.dart';
class MyMenuController extends GetxController {
bool isDrawerOpen = true;
void getDrawerMenu() {
if (isDrawerOpen == true) {
isDrawerOpen = false;
} else {
isDrawerOpen = true;
}
update();
}
}

View File

@@ -0,0 +1,111 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/constant/links.dart';
import 'package:siro_rider/controller/functions/crud.dart';
import 'package:siro_rider/main.dart';
class CaptainWalletController extends GetxController {
bool isLoading = false;
Map walletDate = {};
Map walletDateVisa = {};
Map walletDriverPointsDate = {};
final formKey = GlobalKey<FormState>();
String totalAmount = '0';
String totalAmountVisa = '0';
String totalPoints = '0';
final amountFromBudgetController = TextEditingController();
payFromBudget() async {
if (formKey.currentState!.validate()) {
var pointFromBudget = box.read(BoxName.countryCode) == 'Jordan'
? int.parse((amountFromBudgetController.text)) * 100
: int.parse((amountFromBudgetController.text));
await addDriverPayment('fromBudgetToPoints',
int.parse((amountFromBudgetController.text)) * -1);
Future.delayed(const Duration(seconds: 2));
await addDriverWallet('fromBudget', pointFromBudget.toString());
update();
Get.back();
// getCaptainWalletFromRide();
// getCaptainWalletFromBuyPoints();
// checkAccountCaptainBank();
}
}
// Future getCaptainWalletFromRide() async {
// isLoading = true;
// update();
// var res = await CRUD().get(
// link: AppLink.getAllPaymentFromRide,
// payload: {'driverID': box.read(BoxName.driverID)},
// );
// walletDate = jsonDecode(res);
// totalAmount = walletDate['message'][0]['total_amount'].toString() == null
// ? '0'
// : walletDate['message'][0]['total_amount'];
// var res1 = await CRUD().get(
// link: AppLink.getAllPaymentVisa,
// payload: {'driverID': box.read(BoxName.driverID)});
// walletDateVisa = jsonDecode(res1);
// totalAmountVisa = walletDateVisa['message'][0]['diff'].toString() == null
// ? '0'
// : walletDateVisa['message'][0]['diff'];
// isLoading = false;
// update();
// }
// Future getCaptainWalletFromBuyPoints() async {
// isLoading = true;
// update();
// var res = await CRUD().get(
// link: AppLink.getDriverPaymentPoints,
// payload: {'driverID': box.read(BoxName.driverID)},
// );
// walletDriverPointsDate = jsonDecode(res);
// if (walletDriverPointsDate['message'][0]['driverID'].toString() ==
// box.read(BoxName.driverID)) {
// double totalPointsDouble = double.parse(
// walletDriverPointsDate['message'][0]['total_amount'].toString());
// totalPoints = totalPointsDouble.toStringAsFixed(0);
// } else {
// totalPoints = '0';
// }
// isLoading = false;
// update();
// }
late String paymentID;
Future addDriverPayment(String paymentMethod, amount) async {
var res =
await CRUD().postWallet(link: AppLink.addDriverPaymentPoints, payload: {
'driverID': box.read(BoxName.driverID).toString(),
'amount': amount.toString(),
'payment_method': paymentMethod.toString(),
});
var d = jsonDecode(res);
paymentID = d['message'].toString();
}
Future addDriverWallet(String paymentMethod, point) async {
await CRUD().postWallet(link: AppLink.addDriversWalletPoints, payload: {
'driverID': box.read(BoxName.driverID).toString(),
'paymentID': paymentID.toString(),
'amount': point,
'paymentMethod': paymentMethod.toString(),
});
}
@override
void onInit() {
// getCaptainWalletFromRide();
// getCaptainWalletFromBuyPoints();
// checkAccountCaptainBank();
super.onInit();
}
}

View File

@@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../constant/box_name.dart';
import '../../functions/digit_obsecur_formate.dart';
import '../../functions/secure_storage.dart';
class CreditCardController extends GetxController {
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
final TextEditingController cardNumberController = TextEditingController();
final TextEditingController cardHolderNameController =
TextEditingController();
final TextEditingController expiryDateController = TextEditingController();
final TextEditingController cvvCodeController = TextEditingController();
openPayment() async {
String? cardNumber = await SecureStorage().readData(BoxName.cardNumber);
String? cardHolderName =
await SecureStorage().readData(BoxName.cardHolderName);
String? expiryDate = await SecureStorage().readData(BoxName.expiryDate);
String? cvvCode = await SecureStorage().readData(BoxName.cvvCode);
// if (cvvCode != null && cvvCode.isNotEmpty) {
// final maskedCardNumber = DigitObscuringFormatter()
// .formatEditUpdate(
// TextEditingValue.empty,
// TextEditingValue(text: cardNumber ?? ''),
// )
// .text;
// cardNumberController.text = maskedCardNumber;
// cardHolderNameController.text = cardHolderName ?? '';
// expiryDateController.text = expiryDate ?? '';
// cvvCodeController.text = cvvCode;
// }
}
@override
void onInit() async {
super.onInit();
openPayment();
// String? cardNumber = await SecureStorage().readData(BoxName.cardNumber);
// String? cardHolderName =
// await SecureStorage().readData(BoxName.cardHolderName);
// String? expiryDate = await SecureStorage().readData(BoxName.expiryDate);
// String? cvvCode = await SecureStorage().readData(BoxName.cvvCode);
// if (cvvCode != null && cvvCode.isNotEmpty) {
// final maskedCardNumber = DigitObscuringFormatter()
// .formatEditUpdate(
// TextEditingValue.empty,
// TextEditingValue(text: cardNumber ?? ''),
// )
// .text;
// cardNumberController.text = maskedCardNumber;
// cardHolderNameController.text = cardHolderName ?? '';
// expiryDateController.text = expiryDate ?? '';
// cvvCodeController.text = cvvCode;
// }
}
}
class CreditCardModel {
String cardNumber;
String cardHolderName;
String expiryDate;
String cvvCode;
CreditCardModel({
required this.cardNumber,
required this.cardHolderName,
required this.expiryDate,
required this.cvvCode,
});
}

View File

@@ -0,0 +1,155 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'package:siro_rider/controller/home/map/ride_lifecycle_controller.dart';
import '../../constant/api_key.dart';
import '../../constant/links.dart';
import '../../constant/style.dart';
import '../functions/crud.dart';
import '../functions/location_controller.dart';
class PointsForRiderController extends GetxController {
List<String> locations = [];
String hintTextDestinationPoint = 'Search for your destination'.tr;
TextEditingController placeStartController = TextEditingController();
void addLocation(String location) {
locations.add(location);
update();
}
void getTextFromList(String location) {
locations.add(location);
update();
Get.back();
}
void removeLocation(int index) {
locations.removeAt(index);
update();
}
void onReorder(int oldIndex, int newIndex) {
if (newIndex > oldIndex) {
newIndex -= 1;
update();
}
final item = locations.removeAt(oldIndex);
locations.insert(newIndex, item);
update();
}
}
class LocationModel {
String name;
double lat, lon;
LocationModel({required this.name, required this.lat, required this.lon});
}
class WayPointController extends GetxController {
// A list of text editing controllers for each text field
// final textFields = [TextEditingController()].obs;
List<String> wayPoints = [];
List<List<dynamic>> placeListResponse = [];
double wayPointHeight = 400;
String hintTextDestinationPoint = 'Search for your destination'.tr;
TextEditingController textSearchCotroller = TextEditingController();
// A list of places corresponding to each text field
final places = <String>[];
final hintTextPointList = <String>[];
late LatLng myLocation;
void addWayPoints() {
String wayPoint = 'Add a Stop'.tr;
if (wayPoints.length < 5) {
wayPoints.add(wayPoint);
update();
} else {
Get.defaultDialog(
title: 'This is most WayPoints',
titleStyle: AppStyle.title,
middleText: '');
}
update();
}
void removeTextField(int index) {
wayPoints.removeAt(index);
update();
}
// A method to reorder the text fields and the places
void reorderTextFields(int oldIndex, int newIndex) {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final wayPoint = wayPoints.removeAt(oldIndex);
wayPoints.insert(newIndex, wayPoint);
update();
}
void updatePlace(int index, String input) async {
var url =
'${AppLink.googleMapsLink}place/nearbysearch/json?keyword=$input&location=${myLocation.latitude},${myLocation.longitude}&radius=50000&language=en&key=${AK.mapAPIKEY.toString()}';
var response = await CRUD().getGoogleApi(link: url, payload: {});
// final place = input;
// if (index == 0) {
List<dynamic> newList = [];
placeListResponse.add(newList);
newList = response['results'];
placeListResponse[index].add(newList);
update();
// }
update();
}
@override
void onInit() {
// Get.put(LocationController());
addWayPoints();
myLocation = Get.find<RideLifecycleController>().passengerLocation;
super.onInit();
}
void reset() {
wayPoints.clear();
addWayPoints();
placeListResponse.clear();
update();
}
}
class PlaceList extends StatelessWidget {
// Get the controller instance
final controller = Get.find<WayPointController>();
@override
Widget build(BuildContext context) {
// Use the Obx widget to rebuild the widget when the controller changes
return Obx(() {
// Use the ListView widget to display the list of places
return ListView(
// The children of the list are the places
children: [
// Loop through the places in the controller
for (final place in controller.places)
// Create a text widget for each place
Text(
// Use the place as the text
place,
// Add some style and padding
style: const TextStyle(fontSize: 18.0),
),
],
);
});
}
}

View File

@@ -0,0 +1,159 @@
--- PRECISE MISSING METHODS ---
Total original methods/getters/setters: 270
Total defined in split controllers: 270
Total missing: 53
- Column
- CupertinoDialogAction
- Future
- _applyLowEndModeIfNeeded
- _buildOsrmWaypointCoords
- _calculateDistance
- _checkAndRecalculateIfDeviated
- _fillDriverDataLocally
- _haversineKm
- _initMinimalIcons
- _initializePolygons
- _isActiveRideState
- _kmToLatDelta
- _kmToLngDelta
- _onDriverAcceptedWithSocket
- _onDriverArrivedWithSocket
- _onRideCancelledWithSocket
- _onRideStartedWithSocket
- _playRouteAnimation
- _relevanceScore
- _restorePolyline
- _retryProcess
- _stageNiceToHave
- _stagePricingAndState
- _startMasterTimer
- _startMasterTimerWithInterval
- _startPollingFallback
- _stopDriverLocationPolling
- _updateDriverMarker
- addPostFrameCallback
- cancelRide
- checkPassengerLocation
- currentDriverMarkerId
- detectPerfMode
- directions
- getAIKey
- getDirectionMap
- getDistanceFromDriverAfterAcceptedRide
- getMapPointsForAllMethods
- getPassengerLocationUniversity
- getRideStatus
- handleActiveRideOnStartup
- handleNoDriverFound
- isDriversDataValid
- lastWhere
- onChangedPassengerCount
- onChangedPassengersChoose
- processRideAcceptance
- retrySearchForDrivers
- showDrawingBottomSheet
- showNoDriversDialog
- startSearchingTimer
- wait
--- PRECISE MISSING VARIABLES/FIELDS ---
Total original variables: 626
Total defined in split controllers: 558
Total missing: 97
- EdgeInsets
- Error
- InfoWindow
- LatLngBounds
- LocationData
- R
- _buildOsrmWaypointCoords
- _calculateDistance
- _haversineKm
- _isActiveRideState
- _isStateProcessing
- _isUsingFallback
- _kmToLatDelta
- _kmToLngDelta
- _reconnectTimer
- _relevanceScore
- a
- aerialDistance
- apiDistanceMeters
- apiKey
- attemptCount
- c
- carInfo
- cardNumber
- carsOrder
- checkPassengerLocation
- commissionPct
- context
- coordDestination
- currentAttempt
- currentCarType
- currentLocationOfDrivers
- currentRideId
- currentTimeSearchingCaptainWindow
- dInfo
- dLat
- dataCarsLocationByPassenger
- datadriverCarsLocationToPassengerAfterApplied
- dest
- deviation
- distanceOfTrip
- driverCarPlate
- driverLocationToPassenger
- driverName
- driverOrderStatus
- driverPhone
- durationByPassenger
- dynamicApiUrl
- etaText
- fName
- finalReason
- firebaseMessagesController
- increaseFeeFormKey
- info
- isBeginRideFromDriverRunning
- isDrawingRoute
- isDriversDataValid
- isDriversTokensSend
- isInUniversity
- isRequestValid
- kDurationScalar
- key
- km
- kmInDegree
- lName
- latDest
- latestWaypoint
- lngDest
- lowPerf
- mapAPIKEY
- messagesFormKey
- might
- minBillableKm
- minFareSYP
- newValue
- northeast
- originCoords
- pLower
- passengerLocation
- passengerLocationStringUnvirsity
- placeName
- polylineString
- previousLocationOfDrivers
- progressTimerRideBeginVip
- promoFormKey
- qLower
- query
- rLat1
- rLat2
- ram
- rideData
- sdk
- selectedPassengerCount
- southwest
- startLng
- status
- stringElapsedTimeRideBegin

View File

@@ -0,0 +1,214 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/constant/colors.dart';
import 'package:siro_rider/constant/links.dart';
import 'package:siro_rider/controller/functions/crud.dart';
import 'package:siro_rider/main.dart';
import 'package:http_parser/http_parser.dart';
import 'package:mime/mime.dart';
import '../../../env/env.dart';
import '../../../print.dart';
import '../../../views/widgets/mydialoug.dart';
import '../../functions/encrypt_decrypt.dart';
class ComplaintController extends GetxController {
bool isLoading = false;
final formKey = GlobalKey<FormState>();
final complaintController = TextEditingController();
List feedBack = [];
Map<String, dynamic>? passengerReport;
Map<String, dynamic>? driverReport;
var isUploading = false.obs;
var uploadSuccess = false.obs;
String audioLink = ''; // سيتم تخزين رابط الصوت هنا بعد الرفع
@override
void onInit() {
super.onInit();
getLatestRidesForPassengers();
}
// --- دالة مخصصة لعرض إشعارات Snackbar بشكل جميل ---
void _showCustomSnackbar(String title, String message,
{bool isError = false}) {
Get.snackbar(
'', // العنوان سيتم التعامل معه عبر titleText
'', // الرسالة سيتم التعامل معها عبر messageText
titleText: Text(title.tr,
style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16)),
messageText: Text(message.tr,
style: const TextStyle(color: Colors.white, fontSize: 14)),
backgroundColor: isError
? AppColor.redColor.withOpacity(0.95)
: const Color.fromARGB(255, 6, 148, 79).withOpacity(0.95),
icon: Icon(isError ? Icons.error_outline : Icons.check_circle_outline,
color: Colors.white, size: 28),
borderRadius: 12,
margin: const EdgeInsets.all(15),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 4),
colorText: Colors.white,
);
}
// --- هذه الدالة تبقى كما هي لجلب بيانات الرحلة ---
getLatestRidesForPassengers() async {
isLoading = true;
update();
var res = await CRUD().get(link: AppLink.getFeedBack, payload: {
'passengerId': box.read(BoxName.passengerID).toString(),
});
if (res != 'failure') {
var d = jsonDecode(res)['message'];
feedBack = d;
}
isLoading = false;
update();
}
// --- تم تحديث الهيدر في هذه الدالة ---
Future<void> uploadAudioFile(File audioFile) async {
try {
isUploading.value = true;
update();
var uri = Uri.parse('${AppLink.server}/upload_audio.php');
var request = http.MultipartRequest('POST', uri);
String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
final String fingerPrint = box.read(BoxName.deviceFpEncrypted)?.toString() ?? '';
var mimeType = lookupMimeType(audioFile.path);
// ** التعديل: تم استخدام نفس هيدر التوثيق **
request.headers.addAll({
'Authorization': 'Bearer $token',
'X-Device-FP': fingerPrint,
});
request.files.add(
await http.MultipartFile.fromPath(
'audio',
audioFile.path,
contentType: mimeType != null ? MediaType.parse(mimeType) : null,
),
);
var response = await request.send();
var responseBody = await http.Response.fromStream(response);
if (response.statusCode == 200) {
var jsonResponse = jsonDecode(responseBody.body);
if (jsonResponse['status'] == 'Audio file uploaded successfully.') {
uploadSuccess.value = true;
audioLink = jsonResponse['link']; // تخزين الرابط في المتغير
Get.back();
// استخدام الـ Snackbar الجديد
_showCustomSnackbar('Success', 'Audio uploaded successfully.');
} else {
uploadSuccess.value = false;
_showCustomSnackbar('Error', 'Failed to upload audio file.',
isError: true);
}
} else {
uploadSuccess.value = false;
_showCustomSnackbar('Error', 'Server error: ${response.statusCode}',
isError: true);
}
} catch (e) {
uploadSuccess.value = false;
_showCustomSnackbar(
'Error', 'An application error occurred during upload.',
isError: true);
} finally {
isUploading.value = false;
update();
}
}
// --- الدالة الجديدة التي تتصل بالسكريبت الجديد ---
Future<void> submitComplaintToServer() async {
// 1. التحقق من صحة الفورم
if (!formKey.currentState!.validate() || complaintController.text.isEmpty) {
// استخدام الـ Snackbar الجديد
_showCustomSnackbar(
'Error', 'Please describe your issue before submitting.',
isError: true);
return;
}
// 2. التحقق من وجود بيانات الرحلة
if (feedBack.isEmpty) {
// استخدام الـ Snackbar الجديد
_showCustomSnackbar(
'Error', 'Ride information not found. Please refresh the page.',
isError: true);
return;
}
isLoading = true;
update();
try {
// 3. استخراج البيانات المطلوبة
final rideId = feedBack[0]['id'].toString(); // ! تأكد أن اسم حقل ID صحيح
final complaint = complaintController.text;
String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
// 4. استدعاء سكربت PHP الجديد باستخدام http.post
final response = await http.post(
Uri.parse(AppLink.add_solve_all),
headers: {'Authorization': 'Bearer $token'},
body: {
'ride_id': rideId,
'complaint_text': complaint,
'audio_link': audioLink,
},
);
Log.print('Server Response: ${response.body}');
if (response.statusCode != 200) {
_showCustomSnackbar(
'Error', 'Failed to connect to the server. Please try again.',
isError: true);
return;
}
// 5. التعامل مع محتوى الرد من الخادم
final responseData = jsonDecode(response.body);
if (responseData['status'] == 'success') {
passengerReport = responseData['data']['passenger_response'];
driverReport = responseData['data']['driver_response'];
update();
MyDialogContent().getDialog(
'Success'.tr, Text('Your complaint has been submitted.'.tr), () {
Get.back();
complaintController.clear();
audioLink = '';
formKey.currentState?.reset();
});
} else {
String errorMessage =
responseData['message'] ?? 'An unknown server error occurred'.tr;
_showCustomSnackbar('Submission Failed', errorMessage, isError: true);
}
} catch (e) {
Log.print("Submit Complaint Error: $e");
_showCustomSnackbar('Error', 'An application error occurred.'.tr,
isError: true);
} finally {
isLoading = false;
update();
}
}
}

View File

@@ -0,0 +1,351 @@
import 'dart:convert';
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/constant/colors.dart';
import 'package:siro_rider/constant/links.dart';
import 'package:siro_rider/controller/functions/crud.dart';
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:share_plus/share_plus.dart';
import '../../../main.dart';
import '../../../print.dart';
import '../../../views/widgets/error_snakbar.dart';
import '../../../views/widgets/mydialoug.dart';
import '../../functions/launch.dart';
import '../../notification/notification_captain_controller.dart';
import '../../payment/payment_controller.dart';
class InviteController extends GetxController {
final TextEditingController invitePhoneController = TextEditingController();
List driverInvitationData = [];
List driverInvitationDataToPassengers = [];
String? couponCode;
String? driverCouponCode;
int selectedTab = 0;
PassengerStats passengerStats = PassengerStats();
List<Contact> contacts = <Contact>[];
RxList<Map<String, dynamic>> contactMaps = <Map<String, dynamic>>[].obs;
@override
void onInit() {
super.onInit();
// It's good practice to fetch initial data in onInit or onReady
// fetchDriverStats();
// fetchDriverStatsPassengers();
}
void updateSelectedTab(int index) {
selectedTab = index;
update();
}
// --- Sharing Methods ---
Future<void> shareDriverCode() async {
if (driverCouponCode != null) {
final String shareText = '''
${'Join Intaleq as a driver using my referral code!'.tr}
${'Use code:'.tr} $driverCouponCode
${'Download the Intaleq Driver app now and earn rewards!'.tr}
''';
await Share.share(shareText);
}
}
Future<void> sharePassengerCode() async {
if (couponCode != null) {
final String shareText = '''
${'Get a discount on your first Intaleq ride!'.tr}
${'Use my referral code:'.tr} $couponCode
${'Download the Intaleq app now and enjoy your ride!'.tr}
''';
await Share.share(shareText);
}
}
// --- Data Fetching ---
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) {
Log.print("Error fetching driver stats: $e");
}
}
void fetchDriverStatsPassengers() async {
try {
var response = await CRUD()
.get(link: AppLink.getDriverInvitationToPassengers, payload: {
"driverId": box.read(BoxName.passengerID),
});
if (response != 'failure') {
var data = jsonDecode(response);
driverInvitationDataToPassengers = data['message'];
update();
}
} catch (e) {
Log.print("Error fetching passenger stats: $e");
}
}
// --- Contact Handling ---
/// **IMPROVEMENT**: This function now filters out contacts without any phone numbers.
/// This is the fix for the `RangeError` you were seeing, which happened when the UI
/// tried to access the first phone number of a contact that had none.
Future<void> pickContacts() async {
try {
// 1. Check current permission status using permission_handler for better control
PermissionStatus status = await Permission.contacts.status;
// 2. If status is permanently denied, direct user to settings
if (status.isPermanentlyDenied) {
Get.defaultDialog(
title: 'Permission Required'.tr,
middleText:
'Contact permission is permanently denied. Please enable it in settings to continue.'
.tr,
textConfirm: 'Settings'.tr,
textCancel: 'Cancel'.tr,
confirmTextColor: Colors.white,
onConfirm: () {
openAppSettings();
Get.back();
},
);
return;
}
// 3. Request permission if not already granted
if (!status.isGranted) {
status = await Permission.contacts.request();
}
// 4. Proceed if granted
if (status.isGranted) {
// Also call flutter_contacts requestPermission to ensure it's synced (internal state)
await FlutterContacts.requestPermission(readonly: true);
final List<Contact> allContacts =
await FlutterContacts.getContacts(withProperties: true);
final int totalContactsOnDevice = allContacts.length;
// **FIX**: Filter contacts to only include those with at least one phone number.
contacts = allContacts.where((c) => c.phones.isNotEmpty).toList();
final int contactsWithPhones = contacts.length;
if (contactsWithPhones > 0) {
Log.print('Found $contactsWithPhones contacts with phone numbers.');
contactMaps.value = contacts.map((contact) {
return {
'name': contact.displayName,
'phones': contact.phones.map((p) => p.number).toList(),
'emails': contact.emails.map((e) => e.address).toList(),
};
}).toList();
update();
// **IMPROVEMENT**: Provide feedback if some contacts were filtered out.
if (contactsWithPhones < totalContactsOnDevice) {
// Get.snackbar('Contacts Loaded'.tr,
// '${'Showing'.tr} $contactsWithPhones ${'of'.tr} $totalContactsOnDevice ${'contacts. Others were hidden because they don\'t have a phone number.'.tr}',
// snackPosition: SnackPosition.BOTTOM);
}
} else {
Get.snackbar('No contacts found'.tr,
'No contacts with phone numbers were found on your device.'.tr);
}
} else {
Get.snackbar('Permission denied'.tr,
'Contact permission is required to pick contacts'.tr);
}
} catch (e) {
Log.print('Error picking contacts: $e');
Get.snackbar(
'Error'.tr, 'An error occurred while picking contacts: $e'.tr);
}
}
void selectPhone(String phone) {
invitePhoneController.text = phone;
update();
Get.back();
}
/// **IMPROVEMENT**: A new robust function to format phone numbers specifically for Syria (+963).
/// It handles various user inputs gracefully to produce a standardized international format.
String _formatSyrianPhoneNumber(String phone) {
// 1. Remove all non-digit characters to clean the input.
String digitsOnly = phone.replaceAll(RegExp(r'\D'), '');
// 2. If it already starts with the country code, we assume it's correct.
if (digitsOnly.startsWith('963')) {
return '$digitsOnly';
}
// 3. If it starts with '09' (common local format), remove the leading '0'.
if (digitsOnly.startsWith('09')) {
digitsOnly = digitsOnly.substring(1);
}
// 4. Prepend the Syrian country code.
return '963$digitsOnly';
}
/// **IMPROVEMENT**: This method now uses the new phone formatting logic and
/// sends a much-improved, user-friendly WhatsApp message.
void sendInviteToPassenger() async {
if (invitePhoneController.text.isEmpty ||
invitePhoneController.text.length < 9) {
mySnackeBarError('Please enter a correct phone'.tr);
return;
}
try {
// Use the new formatting function to ensure the number is correct.
String formattedPhoneNumber =
_formatSyrianPhoneNumber(invitePhoneController.text);
var response =
await CRUD().post(link: AppLink.addInvitationPassenger, payload: {
"driverId": box.read(BoxName.passengerID),
"inviterPassengerPhone": formattedPhoneNumber,
});
if (response != 'failure') {
var d = response;
Get.snackbar('Success'.tr, 'Invite sent successfully'.tr,
backgroundColor: Colors.green, colorText: Colors.white);
// التحقق الديناميكي من مكان البيانات (V1 vs V3)
var payload = d['data'] ?? d['message'];
// إذا كان الـ message نصاً وليس خريطة (Map)، نأخذ البيانات من المستوى الأعلى
if (payload is String) {
payload = d;
}
String expirationTime = (payload['expirationTime'] ?? '').toString();
String inviteCode = (payload['inviteCode'] ?? '').toString();
// New and improved WhatsApp message for better user engagement.
String message =
"👋 ${'Hello! I\'m inviting you to try Intaleq.'.tr}\n\n"
"🎁 ${'Use my invitation code to get a special gift on your first ride!'.tr}\n\n"
"${'Your personal invitation code is:'.tr}\n"
"*$inviteCode*\n\n"
"${'Be sure to use it quickly! This code expires at'.tr} *$expirationTime*.\n\n"
"📲 ${'Download the app now:'.tr}\n"
"• *Android:* https://play.google.com/store/apps/details?id=com.Intaleq.intaleq\n"
"• *iOS:* https://apps.apple.com/st/app/intaleq-rider/id6748075179\n\n"
"${'See you on the road!'.tr} 🚗";
launchCommunication('whatsapp', formattedPhoneNumber, message);
invitePhoneController.clear();
update();
} else {
Get.snackbar(
'Error'.tr, "This phone number has already been invited.".tr,
backgroundColor: AppColor.redColor,
duration: const Duration(seconds: 4));
}
} catch (e) {
Log.print("Error sending invite: $e");
Get.snackbar(
'Error'.tr, 'An unexpected error occurred. Please try again.'.tr,
backgroundColor: AppColor.redColor);
}
}
// This function is dependent on the `pickContacts` method filtering out contacts without phones.
savePhoneToServer() async {
for (var contactMap in contactMaps) {
// The `pickContacts` function ensures the 'phones' list is not empty here.
var phones = contactMap['phones'] as List<String>;
var res = await CRUD().post(link: AppLink.savePhones, payload: {
"name": contactMap['name'] ?? 'No Name',
"phones": phones.first, // Safely access the first phone number
"phones2": phones.join(', '),
});
if (res == 'failure') {
Log.print('Failed to save contact: ${contactMap['name']}');
}
}
}
void onSelectPassengerInvitation(int index) async {
try {
final invitation = driverInvitationDataToPassengers[index];
final tripCount =
int.tryParse(invitation['countOfInvitDriver'].toString()) ?? 0;
final passengerName = invitation['passengerName'].toString();
final isGiftTaken = invitation['isGiftToken'].toString() == '1';
if (tripCount >= 2) {
// Gift can be claimed
if (!isGiftTaken) {
MyDialog().getDialog(
'You deserve the gift'.tr,
'${'Claim your 20 LE gift for inviting'.tr} $passengerName!',
() async {
Get.back(); // Close dialog first
await Get.find<PaymentController>().addPassengersWallet('20');
await CRUD().post(
link: AppLink.updatePassengerGift,
payload: {'id': invitation['id']},
);
NotificationCaptainController().addNotificationCaptain(
invitation['passengerInviterId'].toString(),
"You have got a gift for invitation".tr,
'${"You have earned 20".tr} ${'LE'}',
false,
);
fetchDriverStatsPassengers(); // Refresh list
},
);
} else {
MyDialog().getDialog(
"Gift Already Claimed".tr,
"You have already received your gift for inviting $passengerName."
.tr,
() => Get.back(),
);
}
} else {
// Gift not yet earned
MyDialog().getDialog(
'${'Keep it up!'.tr}',
'$passengerName ${'has completed'.tr} $tripCount / 2 ${'trips'.tr}. ${"You can claim your gift once they complete 2 trips.".tr}',
() => Get.back(),
);
}
} catch (e) {
Log.print("Error in onSelectPassengerInvitation: $e");
}
}
}
class PassengerStats {
final int totalInvites;
final int activeUsers;
final double totalEarnings;
PassengerStats({
this.totalInvites = 0,
this.activeUsers = 0,
this.totalEarnings = 0.0,
});
}

View File

@@ -0,0 +1,35 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/constant/links.dart';
import 'package:siro_rider/controller/functions/crud.dart';
import 'package:siro_rider/main.dart';
class OrderHistoryController extends GetxController {
List<dynamic> orderHistoryListPassenger = [];
bool isloading = true;
@override
void onInit() {
getOrderHistoryByPassenger();
super.onInit();
}
Future getOrderHistoryByPassenger() async {
var res = await CRUD().get(link: AppLink.getRides, payload: {
'passenger_id': box.read(BoxName.passengerID).toString(),
});
if (res.toString() == 'failure') {
// Get.snackbar('failure', 'message');
isloading = false;
update();
} else {
var jsonDecoded = jsonDecode(res);
var rawData = jsonDecoded['data'] ?? jsonDecoded['message'];
orderHistoryListPassenger = rawData is List ? rawData : [];
isloading = false;
update();
}
}
}

View File

@@ -0,0 +1,45 @@
import 'dart:convert';
import 'package:siro_rider/constant/box_name.dart';
import 'package:get/get.dart';
import 'package:siro_rider/constant/links.dart';
import 'package:siro_rider/controller/functions/crud.dart';
import '../../../main.dart';
class PromosController extends GetxController {
List<dynamic> promoList = [];
bool isLoading = true;
late String promos;
@override
void onInit() {
getPromoByToday();
super.onInit();
}
Future getPromoByToday() async {
var res = await CRUD().get(link: AppLink.getPromoBytody, payload: {
'passengerID': box.read(BoxName.passengerID).toString(),
});
if (res.toString() == 'failure') {
// Get.defaultDialog(
// title: 'No Promo for today .'.tr,
// middleText: '',
// titleStyle: AppStyle.title,
// confirm: MyElevatedButton(
// title: 'Back'.tr,
// onPressed: () {
// Get.back();
// Get.back();
// }));
isLoading = false;
update();
} else {
var jsonDecoded = jsonDecode(res);
promoList = jsonDecoded['message'];
isLoading = false;
update();
}
}
}

View File

@@ -0,0 +1,233 @@
import 'package:siro_rider/print.dart';
import 'dart:async';
import 'dart:math';
import 'package:siro_rider/controller/functions/crud.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/onbording_page.dart';
import 'package:siro_rider/views/auth/login_page.dart';
import 'package:siro_rider/controller/auth/login_controller.dart';
import 'package:siro_rider/controller/functions/secure_storage.dart';
import 'package:quick_actions/quick_actions.dart';
import '../../../constant/notification.dart';
import '../../../main.dart';
import '../firebase/firbase_messge.dart';
import '../firebase/local_notification.dart';
import '../functions/encrypt_decrypt.dart';
class SplashScreenController extends GetxController
with GetTickerProviderStateMixin {
// ─── انيميشن الـ splash الأصلي ───────────────────────────────────────────
late AnimationController _animationController;
late Animation<double> titleFadeAnimation,
titleScaleAnimation,
taglineFadeAnimation,
footerFadeAnimation;
late Animation<Offset> taglineSlideAnimation;
// ─── انيميشن الحلقات المدارية ────────────────────────────────────────────
late AnimationController _orbitController;
late Animation<double> orbitAnimation;
// ─── انيميشن التوهج المتنفّس ─────────────────────────────────────────────
late AnimationController _glowController;
late Animation<double> glowAnimation;
final progress = 0.0.obs;
Timer? _progressTimer;
@override
void onInit() {
super.onInit();
// ── الكنترولر الرئيسي للـ splash ─────────────────────────────────────
_animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 2000));
titleFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.5, curve: Curves.easeOut)));
titleScaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.5, curve: Curves.easeOut)));
taglineFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(0.3, 0.8, curve: Curves.easeOut)));
taglineSlideAnimation =
Tween<Offset>(begin: const Offset(0, 0.5), end: Offset.zero).animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(0.3, 0.8, curve: Curves.easeOut)));
footerFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(0.5, 1.0, curve: Curves.easeOut)));
// ── كنترولر الدوران المداري — دورة كاملة كل 7 ثوانٍ ─────────────────
_orbitController =
AnimationController(vsync: this, duration: const Duration(seconds: 7))
..repeat();
orbitAnimation =
Tween<double>(begin: 0.0, end: 1.0).animate(_orbitController);
// ── كنترولر التوهج المتنفّس — نبضة كل ثانيتين ───────────────────────
_glowController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 2000))
..repeat(reverse: true);
glowAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _glowController, curve: Curves.easeInOut));
_animationController.forward();
_initializeApp();
}
Future<void> _initializeApp() async {
_startProgressAnimation();
// Run navigation logic and background services in parallel.
final logicFuture = _performNavigationLogic();
final backgroundServicesFuture = _initializeBackgroundServices();
// Ensure the splash screen is visible for a minimum duration.
final minTimeFuture = Future.delayed(const Duration(seconds: 3));
// Wait for all tasks to complete.
await Future.wait([logicFuture, backgroundServicesFuture, minTimeFuture]);
}
void _startProgressAnimation() {
// Visual timer for the progress bar.
const totalTime = 2800; // ms
const interval = 50; // ms
int elapsed = 0;
_progressTimer =
Timer.periodic(const Duration(milliseconds: interval), (timer) {
elapsed += interval;
progress.value = (elapsed / totalTime).clamp(0.0, 1.0);
if (elapsed >= totalTime) timer.cancel();
});
}
/// Initializes all heavy background services while the splash animation is running.
Future<void> _initializeBackgroundServices() async {
try {
await EncryptionHelper.initialize();
// --- [LAZY LOADING IN ACTION] ---
// This `Get.find()` call will create the NotificationController instance
// for the first time because it was defined with `lazyPut`.
final notificationController = Get.find<NotificationController>();
await notificationController.initNotifications();
// The same happens for FirebaseMessagesController.
final fcm = Get.find<FirebaseMessagesController>();
await fcm.requestFirebaseMessagingPermission();
// _scheduleDailyNotifications(notificationController);
_initializeQuickActions();
} catch (e, st) {
CRUD.addError('background_init_error: $e', st.toString(), 'SplashScreen');
}
}
/// Determines the next screen based on user's login state.
Future<void> _performNavigationLogic() async {
await _getPackageInfo();
SecureStorage().saveData('iss', 'mobile-app:');
// ── [OPTIMIZATION] جلب التوكن عند بداية تشغيل التطبيق ────────────────
Log.print("SplashScreen: Initializing JWT...");
await Get.find<LoginController>().getJWT();
if (box.read(BoxName.onBoarding) == null) {
Get.off(() => OnBoardingPage());
} else if (box.read(BoxName.email) != null &&
box.read(BoxName.phone) != null &&
box.read(BoxName.isVerified) == '1') {
// `Get.find()` creates the LoginController instance here.
final loginController = Get.find<LoginController>();
// The loginController itself will handle navigation via Get.offAll() upon success.
await loginController.loginUsingCredentials(
box.read(BoxName.passengerID).toString(),
box.read(BoxName.email).toString(),
);
} else {
Get.off(() => LoginPage());
}
}
Future<void> _getPackageInfo() async {
try {
final info = await PackageInfo.fromPlatform();
box.write(BoxName.packagInfo, info.version);
update();
} catch (e) {
Log.print("Could not get package info: $e");
}
}
void _scheduleDailyNotifications(NotificationController controller) {
try {
final List<String> msgs = passengerMessages ?? const [];
if (msgs.isEmpty) {
controller.scheduleNotificationsForSevenDays(
'Intaleq', 'مرحباً بك! تابع رحلاتك بأمان مع انطلق.', "tone1");
} else {
final rnd = Random();
final raw = msgs[rnd.nextInt(msgs.length)];
final parts = raw.split(':');
final title = parts.isNotEmpty ? parts.first.trim() : 'Intaleq';
final body = parts.length > 1
? parts.sublist(1).join(':').trim()
: 'مرحباً بك! تابع رحلاتك بأمان مع انطلق.';
controller.scheduleNotificationsForSevenDays(
title.isEmpty ? 'Intaleq' : title,
body.isEmpty ? 'مرحباً بك! تابع رحلاتك بأمان مع انطلق.' : body,
"tone1");
}
} catch (e, st) {
CRUD.addError('notif_init: $e', st.toString(), 'SplashScreen');
}
}
void _initializeQuickActions() {
final QuickActions quickActions = QuickActions();
quickActions.initialize((String shortcutType) {
Get.toNamed('/$shortcutType');
});
quickActions.setShortcutItems(<ShortcutItem>[
ShortcutItem(
type: 'shareApp', localizedTitle: 'Share App'.tr, icon: 'icon_share'),
ShortcutItem(
type: 'wallet', localizedTitle: 'Wallet'.tr, icon: 'icon_wallet'),
ShortcutItem(
type: 'profile', localizedTitle: 'Profile'.tr, icon: 'icon_user'),
ShortcutItem(
type: 'contactSupport',
localizedTitle: 'Contact Support'.tr,
icon: 'icon_support'),
]);
}
@override
void onClose() {
_progressTimer?.cancel();
_animationController.dispose();
_orbitController.dispose();
_glowController.dispose();
super.onClose();
}
}

View File

@@ -0,0 +1,107 @@
import 'dart:async';
import 'dart:convert';
import 'package:siro_rider/constant/links.dart';
import 'package:siro_rider/controller/functions/crud.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
class TripMonitorController extends GetxController {
bool isLoading = false;
Map tripData = {};
late String rideId;
late String driverId;
IntaleqMapController? mapController;
List myListString = [];
late Timer timer;
late LatLng parentLocation;
String carIcon = 'car';
String motoIcon = 'moto';
String ladyIcon = 'lady';
double rotation = 0;
double speed = 0;
bool isStyleLoaded = false;
Set<Marker> markers = {};
getLocationParent() async {
var res = await CRUD().get(
link: AppLink.getLocationParents, payload: {"driver_id": driverId});
if (res != 'failure') {
tripData = jsonDecode(res);
parentLocation = LatLng(
double.parse(tripData['message'][0]['latitude'].toString()),
double.parse(tripData['message'][0]['longitude'].toString()));
rotation = double.parse(tripData['message'][0]['heading'].toString());
speed = double.parse(tripData['message'][0]['speed'].toString());
_updateMarker();
update();
}
}
void onMapCreated(IntaleqMapController controller) async {
mapController = controller;
update();
}
void onStyleLoaded() async {
isStyleLoaded = true;
mapController?.animateCamera(
CameraUpdate.newLatLng(parentLocation),
);
_updateMarker();
// Set up a timer or interval to trigger the marker update every 10 seconds.
timer = Timer.periodic(const Duration(seconds: 10), (_) async {
await getLocationParent();
mapController?.animateCamera(CameraUpdate.newLatLng(parentLocation));
update();
});
}
void _updateMarker() {
String iconPath = 'assets/images/car.png';
if (tripData['message'] != null && tripData['message'].isNotEmpty) {
final model = tripData['message'][0]['model'].toString();
final gender = tripData['message'][0]['gender'].toString();
if (model.contains('دراجة')) {
iconPath = 'assets/images/moto1.png';
} else if (gender == 'Female') {
iconPath = 'assets/images/lady1.png';
}
}
markers = {
Marker(
markerId: const MarkerId('driver'),
position: parentLocation,
icon: InlqBitmap.fromAsset(iconPath),
rotation: rotation,
anchor: const Offset(0.5, 0.5),
),
};
update();
}
Future<void> init({String? rideId, String? driverId}) async {
this.driverId = driverId!;
this.rideId = rideId!;
await getLocationParent();
update();
}
@override
void onInit() {
super.onInit();
}
@override
void onClose() {
timer.cancel();
mapController = null;
super.onClose();
}
}

View File

@@ -0,0 +1,324 @@
import 'dart:async';
import 'dart:convert';
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/constant/colors.dart';
import 'package:siro_rider/constant/links.dart';
import 'package:siro_rider/controller/home/map/ride_lifecycle_controller.dart';
import 'package:siro_rider/main.dart';
import 'package:siro_rider/views/widgets/elevated_btn.dart';
import 'package:siro_rider/views/widgets/my_scafold.dart';
import 'package:siro_rider/views/widgets/mycircular.dart';
import 'package:flutter/material.dart';
import 'package:flutter_font_icons/flutter_font_icons.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import '../../constant/style.dart';
import '../functions/crud.dart';
import '../functions/encrypt_decrypt.dart';
class VipOrderController extends GetxController {
RxBool isLoading = false.obs;
final arguments = Get.arguments;
RxList<dynamic> tripData = <dynamic>[].obs;
RxBool isButtonVisible = false.obs;
RxInt countdown = 60.obs;
Timer? _countdownTimer;
@override
void onInit() {
super.onInit();
fetchOrder();
startCountdown();
}
@override
void onClose() {
_countdownTimer?.cancel();
super.onClose();
}
Future<void> fetchOrder() async {
try {
isLoading.value = true;
var mapPassengerController = Get.find<RideLifecycleController>();
var res = await CRUD().get(
link: AppLink.getMishwari,
payload: {
// 'driverId': mapPassengerController.driverIdVip.toString(),
'driverId': box.read(BoxName.passengerID).toString(),
},
);
if (res != 'failure') {
var decodedResponse = jsonDecode(res);
if (decodedResponse['message'] is List) {
tripData.value = decodedResponse['message'];
} else {
tripData.clear(); // Ensure empty list if no data
// mySnackeBarError('No trip data found');
}
} else {
tripData.clear();
// mySnackeBarError('Failed to fetch trip data');
}
} catch (e) {
tripData.clear();
// mySnackeBarError('An error occurred: $e');
} finally {
isLoading.value = false;
}
}
void startCountdown() {
_countdownTimer?.cancel(); // Cancel any existing timer
countdown.value = 60;
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (countdown.value > 0) {
countdown.value--;
} else {
timer.cancel();
isButtonVisible.value = true;
}
});
}
void sendToDriverAgain() {
// Reset states
isButtonVisible.value = false;
fetchOrder(); // Refresh order
startCountdown(); // Restart countdown
}
}
class VipWaittingPage extends StatelessWidget {
VipWaittingPage({super.key});
final VipOrderController vipOrderController = Get.put(VipOrderController());
@override
Widget build(BuildContext context) {
return MyScafolld(
title: "Waiting VIP".tr,
body: [
Obx(() {
// Loading state
if (vipOrderController.isLoading.value) {
return const Center(child: MyCircularProgressIndicator());
}
// No data state
if (vipOrderController.tripData.isEmpty) {
return Center(
child: Text(
'No trip data available'.tr,
style: AppStyle.title,
),
);
}
// Data available
var data = vipOrderController.tripData[0];
// Function to get the localized status string
String getLocalizedStatus(String status) {
switch (status) {
case 'pending':
return 'pending'.tr;
case 'accepted':
return 'accepted'.tr;
case 'begin':
return 'begin'.tr;
case 'rejected':
return 'rejected'.tr;
case 'cancelled':
return 'cancelled'.tr;
default:
return 'unknown'.tr;
}
}
// Function to get the appropriate status color
Color getStatusColor(String status) {
switch (status) {
case 'pending':
return Colors.yellow;
case 'accepted':
return Colors.green;
case 'begin':
return Colors.green;
case 'rejected':
return Colors.red;
case 'cancelled':
return Colors.red;
default:
return Colors.grey;
}
}
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${'Driver Name:'.tr} ${data['name']}",
style: AppStyle.title,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${'Car Plate:'.tr} ${data['car_plate']}",
style: AppStyle.title,
),
Text(
"${'Car Make:'.tr} ${data['make']}",
style: AppStyle.title,
),
Text(
"${'Car Model:'.tr} ${data['model']}",
style: AppStyle.title,
),
Text(
"${"Car Color:".tr} ${data['color']}",
style: AppStyle.title,
),
],
),
SizedBox(
width: 100,
height: 100,
child: Icon(
Fontisto.car,
size: 80,
color: Color(
int.parse(
data['color_hex'].replaceFirst('#', '0xff'),
),
),
),
),
],
),
const SizedBox(height: 12),
const Divider(),
const SizedBox(height: 12),
Container(
color: getStatusColor(data['status']),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"${'Trip Status:'.tr} ${getLocalizedStatus(data['status'])}",
style: const TextStyle(fontSize: 16),
),
),
),
Text(
"${'Scheduled Time:'.tr} ${DateFormat('yyyy-MM-dd hh:mm a').format(DateTime.parse(data['timeSelected']))}",
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
data['status'].toString() != 'begin'
? MyElevatedButton(
title: "Cancel Trip".tr,
kolor: AppColor.redColor,
onPressed: () {
Get.find<RideLifecycleController>().cancelVip(
data['token'].toString(),
data['id'].toString(),
);
},
)
: const SizedBox(),
Obx(() {
// If countdown is still running, show countdown
if (!vipOrderController.isButtonVisible.value) {
return Column(
children: [
CircularProgressIndicator(
value: 1 -
(vipOrderController.countdown.value / 60),
strokeWidth: 6.0,
color: AppColor.greenColor,
backgroundColor: AppColor.accentColor,
),
const SizedBox(height: 10),
Text(
"${vipOrderController.countdown.value}s ${'remaining'.tr}",
style: const TextStyle(fontSize: 16),
),
],
);
}
// Once countdown is complete, show "Send to Driver Again" button
return MyElevatedButton(
title: "Send to Driver Again".tr,
kolor: AppColor.greenColor,
onPressed: () {
Get.find<RideLifecycleController>()
.sendToDriverAgain(data['token']);
vipOrderController.fetchOrder();
},
);
}),
],
),
const SizedBox(
height: 30,
),
data['status'].toString() == 'begin'
? MyElevatedButton(
title: "Click here to begin your trip\n\nGood luck, "
.tr +
(box.read(BoxName.name).toString().split(' ')[0])
.toString(),
kolor: AppColor.greenColor,
onPressed: () {
final mapPassengerController =
Get.find<RideLifecycleController>();
mapPassengerController.make = data['make'];
mapPassengerController.licensePlate =
data['car_plate'];
mapPassengerController.model = data['model'];
mapPassengerController.driverId = data['driverId'];
mapPassengerController.carColor = data['color'];
mapPassengerController.driverRate = data['rating'];
mapPassengerController.colorHex = data['color_hex'];
mapPassengerController.driverPhone = data['phone'];
mapPassengerController.driverToken = data['token'];
mapPassengerController.driverName =
data['name'].toString().split(' ')[0];
Get.back();
mapPassengerController.begiVIPTripFromPassenger();
},
)
: const SizedBox()
],
),
),
);
}),
],
isleading: true,
);
}
}

View File

@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../constant/box_name.dart';
import '../../main.dart';
import '../themes/themes.dart';
class LocaleController extends GetxController {
Locale? language;
String countryCode = '';
ThemeMode themeMode = ThemeMode.system;
void changeLang(String langcode) {
Locale newLocale;
ThemeData newTheme;
bool isArabic = langcode.startsWith('ar');
if (isArabic) {
newLocale = const Locale("ar");
newTheme = Get.isDarkMode ? darkThemeArabic : lightThemeArabic;
} else {
newLocale = const Locale("en");
newTheme = Get.isDarkMode ? darkThemeEnglish : lightThemeEnglish;
}
box.write(BoxName.lang, langcode);
language = newLocale;
Get.changeTheme(newTheme);
Get.updateLocale(newLocale);
update();
}
void changeThemeMode(ThemeMode mode) {
themeMode = mode;
Get.changeThemeMode(mode);
// Explicitly update ThemeData to ensure immediate font and color changes
bool goDark = mode == ThemeMode.dark ||
(mode == ThemeMode.system && Get.isPlatformDarkMode);
bool isArabic = (language?.languageCode ?? 'en').startsWith('ar');
ThemeData newTheme;
if (isArabic) {
newTheme = goDark ? darkThemeArabic : lightThemeArabic;
} else {
newTheme = goDark ? darkThemeEnglish : lightThemeEnglish;
}
Get.changeTheme(newTheme);
box.write(BoxName.themeMode, mode.toString());
update();
}
@override
void onInit() {
String? storedLang = box.read(BoxName.lang);
if (storedLang == null) {
// Use device language if no language is stored
storedLang = Get.deviceLocale!.languageCode;
box.write(BoxName.lang, storedLang);
}
String? storedTheme = box.read(BoxName.themeMode);
if (storedTheme != null) {
if (storedTheme == ThemeMode.light.toString()) {
themeMode = ThemeMode.light;
} else if (storedTheme == ThemeMode.dark.toString()) {
themeMode = ThemeMode.dark;
} else {
themeMode = ThemeMode.system;
}
}
changeLang(storedLang);
super.onInit();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,168 @@
import 'package:siro_rider/controller/local/phone_intel/helpers.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'countries.dart';
class PickerDialogStyle {
final Color? backgroundColor;
final TextStyle? countryCodeStyle;
final TextStyle? countryNameStyle;
final Widget? listTileDivider;
final EdgeInsets? listTilePadding;
final EdgeInsets? padding;
final Color? searchFieldCursorColor;
final InputDecoration? searchFieldInputDecoration;
final EdgeInsets? searchFieldPadding;
final double? width;
PickerDialogStyle({
this.backgroundColor,
this.countryCodeStyle,
this.countryNameStyle,
this.listTileDivider,
this.listTilePadding,
this.padding,
this.searchFieldCursorColor,
this.searchFieldInputDecoration,
this.searchFieldPadding,
this.width,
});
}
class CountryPickerDialog extends StatefulWidget {
final List<Country> countryList;
final Country selectedCountry;
final ValueChanged<Country> onCountryChanged;
final String searchText;
final List<Country> filteredCountries;
final PickerDialogStyle? style;
final String languageCode;
const CountryPickerDialog({
Key? key,
required this.searchText,
required this.languageCode,
required this.countryList,
required this.onCountryChanged,
required this.selectedCountry,
required this.filteredCountries,
this.style,
}) : super(key: key);
@override
State<CountryPickerDialog> createState() => _CountryPickerDialogState();
}
class _CountryPickerDialogState extends State<CountryPickerDialog> {
late List<Country> _filteredCountries;
late Country _selectedCountry;
@override
void initState() {
_selectedCountry = widget.selectedCountry;
_filteredCountries = widget.filteredCountries.toList()
..sort(
(a, b) => a
.localizedName(widget.languageCode)
.compareTo(b.localizedName(widget.languageCode)),
);
super.initState();
}
@override
Widget build(BuildContext context) {
final mediaWidth = MediaQuery.of(context).size.width;
final width = widget.style?.width ?? mediaWidth;
const defaultHorizontalPadding = 40.0;
const defaultVerticalPadding = 24.0;
return Dialog(
insetPadding: EdgeInsets.symmetric(
vertical: defaultVerticalPadding,
horizontal: mediaWidth > (width + defaultHorizontalPadding * 2)
? (mediaWidth - width) / 2
: defaultHorizontalPadding),
backgroundColor: widget.style?.backgroundColor,
child: Container(
padding: widget.style?.padding ?? const EdgeInsets.all(10),
child: Column(
children: <Widget>[
Padding(
padding:
widget.style?.searchFieldPadding ?? const EdgeInsets.all(0),
child: TextField(
cursorColor: widget.style?.searchFieldCursorColor,
decoration: widget.style?.searchFieldInputDecoration ??
InputDecoration(
suffixIcon: const Icon(Icons.search),
labelText: widget.searchText,
),
onChanged: (value) {
_filteredCountries = widget.countryList.stringSearch(value)
..sort(
(a, b) => a
.localizedName(widget.languageCode)
.compareTo(b.localizedName(widget.languageCode)),
);
if (mounted) setState(() {});
},
),
),
const SizedBox(height: 20),
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: _filteredCountries.length,
itemBuilder: (ctx, index) => Column(
children: <Widget>[
ListTile(
leading: kIsWeb
? Image.asset(
'assets/flags/${_filteredCountries[index].code.toLowerCase()}.png',
package: 'intl_phone_field',
width: 32,
)
: Text(
_filteredCountries[index].flag,
style: const TextStyle(fontSize: 18),
),
contentPadding: widget.style?.listTilePadding,
title: Text(
_filteredCountries[index]
.localizedName(widget.languageCode),
style: widget.style?.countryNameStyle ??
const TextStyle(fontWeight: FontWeight.w700),
),
trailing: Text(
'+${_filteredCountries[index].dialCode}',
style: widget.style?.countryCodeStyle ??
const TextStyle(fontWeight: FontWeight.w700),
),
onTap: () {
_selectedCountry = _filteredCountries[index];
widget.onCountryChanged(_selectedCountry);
Navigator.of(context).pop();
},
),
widget.style?.listTileDivider ??
const Divider(thickness: 1),
],
),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,31 @@
import 'countries.dart';
bool isNumeric(String s) =>
s.isNotEmpty && int.tryParse(s.replaceAll("+", "")) != null;
String removeDiacritics(String str) {
var withDia =
'ÀÁÂÃÄÅàáâãäåÒÓÔÕÕÖØòóôõöøÈÉÊËèéêëðÇçÐÌÍÎÏìíîïÙÚÛÜùúûüÑñŠšŸÿýŽž';
var withoutDia =
'AAAAAAaaaaaaOOOOOOOooooooEEEEeeeeeCcDIIIIiiiiUUUUuuuuNnSsYyyZz';
for (int i = 0; i < withDia.length; i++) {
str = str.replaceAll(withDia[i], withoutDia[i]);
}
return str;
}
extension CountryExtensions on List<Country> {
List<Country> stringSearch(String search) {
search = removeDiacritics(search.toLowerCase());
return where(
(country) => isNumeric(search) || search.startsWith("+")
? country.dialCode.contains(search)
: removeDiacritics(country.name.replaceAll("+", "").toLowerCase())
.contains(search) ||
country.nameTranslations.values.any((element) =>
removeDiacritics(element.toLowerCase()).contains(search)),
).toList();
}
}

View File

@@ -0,0 +1,522 @@
library intl_phone_field;
import 'package:siro_rider/print.dart';
import 'dart:async';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import './countries.dart';
import './phone_number.dart';
import 'country_picker_dialog.dart';
import 'helpers.dart';
class IntlPhoneField extends StatefulWidget {
/// The TextFormField key.
final GlobalKey<FormFieldState>? formFieldKey;
/// Whether to hide the text being edited (e.g., for passwords).
final bool obscureText;
/// How the text should be aligned horizontally.
final TextAlign textAlign;
/// How the text should be aligned vertically.
final TextAlignVertical? textAlignVertical;
final VoidCallback? onTap;
/// {@macro flutter.widgets.editableText.readOnly}
final bool readOnly;
final FormFieldSetter<PhoneNumber>? onSaved;
/// {@macro flutter.widgets.editableText.onChanged}
///
/// See also:
///
/// * [inputFormatters], which are called before [onChanged]
/// runs and can validate and change ("format") the input value.
/// * [onEditingComplete], [onSubmitted], [onSelectionChanged]:
/// which are more specialized input change notifications.
final ValueChanged<PhoneNumber>? onChanged;
final ValueChanged<Country>? onCountryChanged;
/// An optional method that validates an input. Returns an error string to display if the input is invalid, or null otherwise.
///
/// A [PhoneNumber] is passed to the validator as argument.
/// The validator can handle asynchronous validation when declared as a [Future].
/// Or run synchronously when declared as a [Function].
///
/// By default, the validator checks whether the input number length is between selected country's phone numbers min and max length.
/// If `disableLengthCheck` is not set to `true`, your validator returned value will be overwritten by the default validator.
/// But, if `disableLengthCheck` is set to `true`, your validator will have to check phone number length itself.
final FutureOr<String?> Function(PhoneNumber?)? validator;
/// {@macro flutter.widgets.editableText.keyboardType}
final TextInputType keyboardType;
/// Controls the text being edited.
///
/// If null, this widget will create its own [TextEditingController].
final TextEditingController? controller;
/// Defines the keyboard focus for this widget.
///
/// The [focusNode] is a long-lived object that's typically managed by a
/// [StatefulWidget] parent. See [FocusNode] for more information.
///
/// To give the keyboard focus to this widget, provide a [focusNode] and then
/// use the current [FocusScope] to request the focus:
///
/// ```dart
/// FocusScope.of(context).requestFocus(myFocusNode);
/// ```
///
/// This happens automatically when the widget is tapped.
///
/// To be notified when the widget gains or loses the focus, add a listener
/// to the [focusNode]:
///
/// ```dart
/// focusNode.addListener(() { Log.print(myFocusNode.hasFocus); });
/// ```
///
/// If null, this widget will create its own [FocusNode].
///
/// ## Keyboard
///
/// Requesting the focus will typically cause the keyboard to be shown
/// if it's not showing already.
///
/// On Android, the user can hide the keyboard - without changing the focus -
/// with the system back button. They can restore the keyboard's visibility
/// by tapping on a text field. The user might hide the keyboard and
/// switch to a physical keyboard, or they might just need to get it
/// out of the way for a moment, to expose something it's
/// obscuring. In this case requesting the focus again will not
/// cause the focus to change, and will not make the keyboard visible.
///
/// This widget builds an [EditableText] and will ensure that the keyboard is
/// showing when it is tapped by calling [EditableTextState.requestKeyboard()].
final FocusNode? focusNode;
/// {@macro flutter.widgets.editableText.onSubmitted}
///
/// See also:
///
/// * [EditableText.onSubmitted] for an example of how to handle moving to
/// the next/previous field when using [TextInputAction.next] and
/// [TextInputAction.previous] for [textInputAction].
final void Function(String)? onSubmitted;
/// If false the widget is "disabled": it ignores taps, the [TextFormField]'s
/// [decoration] is rendered in grey,
/// [decoration]'s [InputDecoration.counterText] is set to `""`,
/// and the drop down icon is hidden no matter [showDropdownIcon] value.
///
/// If non-null this property overrides the [decoration]'s
/// [Decoration.enabled] property.
final bool enabled;
/// The appearance of the keyboard.
///
/// This setting is only honored on iOS devices.
///
/// If unset, defaults to the brightness of [ThemeData.brightness].
final Brightness? keyboardAppearance;
/// Initial Value for the field.
/// This property can be used to pre-fill the field.
final String? initialValue;
final String languageCode;
/// 2 letter ISO Code or country dial code.
///
/// ```dart
/// initialCountryCode: 'IN', // India
/// initialCountryCode: '+225', // Côte d'Ivoire
/// ```
final String? initialCountryCode;
/// List of Country to display see countries.dart for format
final List<Country>? countries;
/// The decoration to show around the text field.
///
/// By default, draws a horizontal line under the text field but can be
/// configured to show an icon, label, hint text, and error text.
///
/// Specify null to remove the decoration entirely (including the
/// extra padding introduced by the decoration to save space for the labels).
final InputDecoration decoration;
/// The style to use for the text being edited.
///
/// This text style is also used as the base style for the [decoration].
///
/// If null, defaults to the `subtitle1` text style from the current [Theme].
final TextStyle? style;
/// Disable view Min/Max Length check
final bool disableLengthCheck;
/// Won't work if [enabled] is set to `false`.
final bool showDropdownIcon;
final BoxDecoration dropdownDecoration;
/// The style use for the country dial code.
final TextStyle? dropdownTextStyle;
/// {@macro flutter.widgets.editableText.inputFormatters}
final List<TextInputFormatter>? inputFormatters;
/// The text that describes the search input field.
///
/// When the input field is empty and unfocused, the label is displayed on top of the input field (i.e., at the same location on the screen where text may be entered in the input field).
/// When the input field receives focus (or if the field is non-empty), the label moves above (i.e., vertically adjacent to) the input field.
final String searchText;
/// Position of an icon [leading, trailing]
final IconPosition dropdownIconPosition;
/// Icon of the drop down button.
///
/// Default is [Icon(Icons.arrow_drop_down)]
final Icon dropdownIcon;
/// Whether this text field should focus itself if nothing else is already focused.
final bool autofocus;
/// Autovalidate mode for text form field.
///
/// If [AutovalidateMode.onUserInteraction], this FormField will only auto-validate after its content changes.
/// If [AutovalidateMode.always], it will auto-validate even without user interaction.
/// If [AutovalidateMode.disabled], auto-validation will be disabled.
///
/// Defaults to [AutovalidateMode.onUserInteraction].
final AutovalidateMode? autovalidateMode;
/// Whether to show or hide country flag.
///
/// Default value is `true`.
final bool showCountryFlag;
/// Message to be displayed on autoValidate error
///
/// Default value is `Invalid Mobile Number`.
final String? invalidNumberMessage;
/// The color of the cursor.
final Color? cursorColor;
/// How tall the cursor will be.
final double? cursorHeight;
/// How rounded the corners of the cursor should be.
final Radius? cursorRadius;
/// How thick the cursor will be.
final double cursorWidth;
/// Whether to show cursor.
final bool? showCursor;
/// The padding of the Flags Button.
///
/// The amount of insets that are applied to the Flags Button.
///
/// If unset, defaults to [EdgeInsets.zero].
final EdgeInsetsGeometry flagsButtonPadding;
/// The type of action button to use for the keyboard.
final TextInputAction? textInputAction;
/// Optional set of styles to allow for customizing the country search
/// & pick dialog
final PickerDialogStyle? pickerDialogStyle;
/// The margin of the country selector button.
///
/// The amount of space to surround the country selector button.
///
/// If unset, defaults to [EdgeInsets.zero].
final EdgeInsets flagsButtonMargin;
/// Enable the autofill hint for phone number.
final bool disableAutoFillHints;
/// If null, default magnification configuration will be used.
final TextMagnifierConfiguration? magnifierConfiguration;
const IntlPhoneField({
Key? key,
this.formFieldKey,
this.initialCountryCode,
this.languageCode = 'en',
this.disableAutoFillHints = false,
this.obscureText = false,
this.textAlign = TextAlign.left,
this.textAlignVertical,
this.onTap,
this.readOnly = false,
this.initialValue,
this.keyboardType = TextInputType.phone,
this.controller,
this.focusNode,
this.decoration = const InputDecoration(),
this.style,
this.dropdownTextStyle,
this.onSubmitted,
this.validator,
this.onChanged,
this.countries,
this.onCountryChanged,
this.onSaved,
this.showDropdownIcon = true,
this.dropdownDecoration = const BoxDecoration(),
this.inputFormatters,
this.enabled = true,
this.keyboardAppearance,
@Deprecated('Use searchFieldInputDecoration of PickerDialogStyle instead')
this.searchText = 'Search country',
this.dropdownIconPosition = IconPosition.leading,
this.dropdownIcon = const Icon(Icons.arrow_drop_down),
this.autofocus = false,
this.textInputAction,
this.autovalidateMode = AutovalidateMode.onUserInteraction,
this.showCountryFlag = true,
this.cursorColor,
this.disableLengthCheck = false,
this.flagsButtonPadding = EdgeInsets.zero,
this.invalidNumberMessage = 'Invalid Mobile Number',
this.cursorHeight,
this.cursorRadius = Radius.zero,
this.cursorWidth = 2.0,
this.showCursor = true,
this.pickerDialogStyle,
this.flagsButtonMargin = EdgeInsets.zero,
this.magnifierConfiguration,
}) : super(key: key);
@override
State<IntlPhoneField> createState() => _IntlPhoneFieldState();
}
class _IntlPhoneFieldState extends State<IntlPhoneField> {
late List<Country> _countryList;
late Country _selectedCountry;
late List<Country> filteredCountries;
late String number;
String? validatorMessage;
@override
void initState() {
super.initState();
_countryList = widget.countries ?? countries;
filteredCountries = _countryList;
number = widget.initialValue ?? '';
if (widget.initialCountryCode == null && number.startsWith('+')) {
number = number.substring(1);
// parse initial value
_selectedCountry = countries.firstWhere(
(country) => number.startsWith(country.fullCountryCode),
orElse: () => _countryList.first);
// remove country code from the initial number value
number = number.replaceFirst(
RegExp("^${_selectedCountry.fullCountryCode}"), "");
} else {
_selectedCountry = _countryList.firstWhere(
(item) => item.code == (widget.initialCountryCode ?? 'US'),
orElse: () => _countryList.first);
// remove country code from the initial number value
if (number.startsWith('+')) {
number = number.replaceFirst(
RegExp("^\\+${_selectedCountry.fullCountryCode}"), "");
} else {
number = number.replaceFirst(
RegExp("^${_selectedCountry.fullCountryCode}"), "");
}
}
if (widget.autovalidateMode == AutovalidateMode.always) {
final initialPhoneNumber = PhoneNumber(
countryISOCode: _selectedCountry.code,
countryCode: '+${_selectedCountry.dialCode}',
number: widget.initialValue ?? '',
);
final value = widget.validator?.call(initialPhoneNumber);
if (value is String) {
validatorMessage = value;
} else {
(value as Future).then((msg) {
validatorMessage = msg;
});
}
}
}
Future<void> _changeCountry() async {
filteredCountries = _countryList;
await showDialog(
context: context,
useRootNavigator: false,
builder: (context) => StatefulBuilder(
builder: (ctx, setState) => CountryPickerDialog(
languageCode: widget.languageCode.toLowerCase(),
style: widget.pickerDialogStyle,
filteredCountries: filteredCountries,
searchText: widget.searchText,
countryList: _countryList,
selectedCountry: _selectedCountry,
onCountryChanged: (Country country) {
_selectedCountry = country;
widget.onCountryChanged?.call(country);
setState(() {});
},
),
),
);
if (mounted) setState(() {});
}
@override
Widget build(BuildContext context) {
return TextFormField(
key: widget.formFieldKey,
initialValue: (widget.controller == null) ? number : null,
autofillHints: widget.disableAutoFillHints
? null
: [AutofillHints.telephoneNumberNational],
readOnly: widget.readOnly,
obscureText: widget.obscureText,
textAlign: widget.textAlign,
textAlignVertical: widget.textAlignVertical,
cursorColor: widget.cursorColor,
onTap: widget.onTap,
controller: widget.controller,
focusNode: widget.focusNode,
cursorHeight: widget.cursorHeight,
cursorRadius: widget.cursorRadius,
cursorWidth: widget.cursorWidth,
showCursor: widget.showCursor,
onFieldSubmitted: widget.onSubmitted,
magnifierConfiguration: widget.magnifierConfiguration,
decoration: widget.decoration.copyWith(
prefixIcon: _buildFlagsButton(),
counterText: !widget.enabled ? '' : null,
),
style: widget.style,
onSaved: (value) {
widget.onSaved?.call(
PhoneNumber(
countryISOCode: _selectedCountry.code,
countryCode:
'+${_selectedCountry.dialCode}${_selectedCountry.regionCode}',
number: value!,
),
);
},
onChanged: (value) async {
final phoneNumber = PhoneNumber(
countryISOCode: _selectedCountry.code,
countryCode: '+${_selectedCountry.fullCountryCode}',
number: value,
);
if (widget.autovalidateMode != AutovalidateMode.disabled) {
validatorMessage = await widget.validator?.call(phoneNumber);
}
widget.onChanged?.call(phoneNumber);
},
validator: (value) {
if (value == null || !isNumeric(value)) return validatorMessage;
if (!widget.disableLengthCheck) {
return value.length >= _selectedCountry.minLength &&
value.length <= _selectedCountry.maxLength
? null
: widget.invalidNumberMessage;
}
return validatorMessage;
},
maxLength: widget.disableLengthCheck ? null : _selectedCountry.maxLength,
keyboardType: widget.keyboardType,
inputFormatters: widget.inputFormatters,
enabled: widget.enabled,
keyboardAppearance: widget.keyboardAppearance,
autofocus: widget.autofocus,
textInputAction: widget.textInputAction,
autovalidateMode: widget.autovalidateMode,
);
}
Container _buildFlagsButton() {
return Container(
margin: widget.flagsButtonMargin,
child: DecoratedBox(
decoration: widget.dropdownDecoration,
child: InkWell(
borderRadius: widget.dropdownDecoration.borderRadius as BorderRadius?,
onTap: widget.enabled ? _changeCountry : null,
child: Padding(
padding: widget.flagsButtonPadding,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const SizedBox(
width: 4,
),
if (widget.enabled &&
widget.showDropdownIcon &&
widget.dropdownIconPosition == IconPosition.leading) ...[
widget.dropdownIcon,
const SizedBox(width: 4),
],
if (widget.showCountryFlag) ...[
kIsWeb
? Image.asset(
'assets/flags/${_selectedCountry.code.toLowerCase()}.png',
package: 'intl_phone_field',
width: 32,
)
: Text(
_selectedCountry.flag,
style: const TextStyle(fontSize: 18),
),
const SizedBox(width: 8),
],
FittedBox(
child: Text(
'+${_selectedCountry.dialCode}',
style: widget.dropdownTextStyle,
),
),
if (widget.enabled &&
widget.showDropdownIcon &&
widget.dropdownIconPosition == IconPosition.trailing) ...[
const SizedBox(width: 4),
widget.dropdownIcon,
],
const SizedBox(width: 8),
],
),
),
),
),
);
}
}
enum IconPosition {
leading,
trailing,
}

View File

@@ -0,0 +1,79 @@
import 'countries.dart';
class NumberTooLongException implements Exception {}
class NumberTooShortException implements Exception {}
class InvalidCharactersException implements Exception {}
class PhoneNumber {
String countryISOCode;
String countryCode;
String number;
PhoneNumber({
required this.countryISOCode,
required this.countryCode,
required this.number,
});
factory PhoneNumber.fromCompleteNumber({required String completeNumber}) {
if (completeNumber == "") {
return PhoneNumber(countryISOCode: "", countryCode: "", number: "");
}
try {
Country country = getCountry(completeNumber);
String number;
if (completeNumber.startsWith('+')) {
number = completeNumber.substring(1 + country.dialCode.length + country.regionCode.length);
} else {
number = completeNumber.substring(country.dialCode.length + country.regionCode.length);
}
return PhoneNumber(
countryISOCode: country.code, countryCode: country.dialCode + country.regionCode, number: number);
} on InvalidCharactersException {
rethrow;
// ignore: unused_catch_clause
} on Exception catch (e) {
return PhoneNumber(countryISOCode: "", countryCode: "", number: "");
}
}
bool isValidNumber() {
Country country = getCountry(completeNumber);
if (number.length < country.minLength) {
throw NumberTooShortException();
}
if (number.length > country.maxLength) {
throw NumberTooLongException();
}
return true;
}
String get completeNumber {
return countryCode + number;
}
static Country getCountry(String phoneNumber) {
if (phoneNumber == "") {
throw NumberTooShortException();
}
final validPhoneNumber = RegExp(r'^[+0-9]*[0-9]*$');
if (!validPhoneNumber.hasMatch(phoneNumber)) {
throw InvalidCharactersException();
}
if (phoneNumber.startsWith('+')) {
return countries
.firstWhere((country) => phoneNumber.substring(1).startsWith(country.dialCode + country.regionCode));
}
return countries.firstWhere((country) => phoneNumber.startsWith(country.dialCode + country.regionCode));
}
@override
String toString() => 'PhoneNumber(countryISOCode: $countryISOCode, countryCode: $countryCode, number: $number)';
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:siro_rider/constant/style.dart';
import 'package:siro_rider/views/widgets/elevated_btn.dart';
import '../../constant/box_name.dart';
import '../../constant/links.dart';
import '../../main.dart';
import '../functions/crud.dart';
class NotificationCaptainController extends GetxController {
bool isLoading = false;
Map notificationData = {};
getNotifications() async {
isLoading = true;
update();
var res = await CRUD().get(
link: AppLink.getNotificationCaptain,
payload: {'driverID': box.read(BoxName.driverID)});
if (res == "failure") {
Get.defaultDialog(
title: 'There is no notification yet'.tr,
titleStyle: AppStyle.title,
middleText: '',
confirm: MyElevatedButton(
title: 'Back',
onPressed: () {
Get.back();
Get.back();
}));
}
notificationData = jsonDecode(res);
// sql.insertData(notificationData['message'], TableName.captainNotification);
isLoading = false;
update();
}
updateNotification(String id) async {
await CRUD().post(
link: AppLink.updateNotificationCaptain,
payload: {'isShown': true, 'id': id},
);
}
addNotificationCaptain(String driverId, title, body, isPin) async {
await CRUD().post(link: AppLink.addNotificationCaptain, payload: {
'driverID': driverId,
'title': title,
'body': body,
'isPin': isPin
});
}
@override
void onInit() {
getNotifications();
super.onInit();
}
}

View File

@@ -0,0 +1,87 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:siro_rider/controller/firebase/firbase_messge.dart';
import '../../constant/box_name.dart';
import '../../constant/links.dart';
import '../../main.dart';
import '../../views/widgets/mydialoug.dart';
import '../firebase/notification_service.dart';
import '../functions/crud.dart';
class PassengerNotificationController extends GetxController {
bool isloading = false;
Map notificationData = {};
getNotifications() async {
isloading = true;
update();
var res = await CRUD().get(
link: AppLink.getNotificationPassenger,
payload: {'passenger_id': box.read(BoxName.passengerID)});
if (res == "failure" || res == "error") {
MyDialog().getDialog('There is no notification yet'.tr, '', () {
Get.back();
Get.back();
});
} else {
final decoded = jsonDecode(res);
// التحقق من وجود البيانات في 'data' أو 'message'
var rawData = decoded['data'] ?? decoded['message'];
if (decoded['status'] == 'error' || decoded['status'] == 'failure' || rawData == "No notification data found") {
notificationData = {'status': 'success', 'message': []}; // قائمة فارغة لمنع الخطأ في UI
} else {
// التأكد أننا نخزن قائمة
notificationData = {
'status': 'success',
'message': rawData is List ? rawData : [rawData]
};
}
isloading = false;
update();
}
// sql.insertData(notificationData['message'], TableName.captainNotification);
}
updateNotification(String id) async {
await CRUD().post(
link: AppLink.updateNotificationPassenger,
payload: {'isShown': 'true', 'id': id},
);
Get.back();
getNotifications();
}
addNotificationToPassenger(String title, body) async {
var res = CRUD().post(link: AppLink.addNotificationPassenger, payload: {
'title': title,
'body': body,
});
// Get.find<FirebaseMessagesController>().sendNotificationToPassengerToken(
// title,
// body,
// 'token',
// [],
// 'iphone_ringtone',
// );
await NotificationService.sendNotification(
category: title,
target: 'token'.toString(),
title: title,
body: body.tr,
isTopic: false, // Important: this is a token
tone: 'cancel',
driverList: [],
);
}
@override
void onInit() {
getNotifications();
super.onInit();
}
}

View File

@@ -0,0 +1,39 @@
import 'dart:convert';
import 'package:siro_rider/constant/style.dart';
import 'package:siro_rider/views/widgets/elevated_btn.dart';
import 'package:get/get.dart';
import '../../constant/links.dart';
import '../functions/crud.dart';
class RideAvailableController extends GetxController {
bool isLoading = false;
Map rideAvailableMap = {};
getRideAvailable() async {
isLoading = true;
var res = await CRUD().get(link: AppLink.getRideWaiting, payload: {});
if (res != 'failure') {
rideAvailableMap = jsonDecode(res);
isLoading = false;
update();
} else {
Get.defaultDialog(
title: 'No Rides now!'.tr,
middleText: '',
titleStyle: AppStyle.title,
confirm: MyElevatedButton(
title: 'Ok'.tr,
onPressed: () {
Get.back();
Get.back();
}));
}
}
@override
void onInit() {
getRideAvailable();
super.onInit();
}
}

View File

@@ -0,0 +1,43 @@
import 'dart:convert';
import 'package:siro_rider/views/widgets/elevated_btn.dart';
import 'package:get/get.dart';
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/constant/links.dart';
import 'package:siro_rider/controller/functions/crud.dart';
import 'package:siro_rider/main.dart';
class DriverWalletHistoryController extends GetxController {
bool isLoading = false;
List archive = [];
getArchivePayment() async {
isLoading = true;
update();
var res = await CRUD().getWallet(
link: AppLink.getWalletByDriver,
payload: {'driverID': box.read(BoxName.driverID)});
if (res == 'failure') {
Get.defaultDialog(
barrierDismissible: false,
title: 'There is no data yet.'.tr,
middleText: '',
confirm: MyElevatedButton(
title: 'Back'.tr,
onPressed: () {
Get.back();
Get.back();
},
));
}
archive = jsonDecode(res)['message'];
isLoading = false;
update();
}
@override
void onInit() {
getArchivePayment();
super.onInit();
}
}

View File

@@ -0,0 +1,48 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/constant/links.dart';
import 'package:siro_rider/controller/functions/crud.dart';
import 'package:siro_rider/main.dart';
import '../../views/widgets/mydialoug.dart';
class PassengerWalletHistoryController extends GetxController {
bool isLoading = false;
List archive = [];
Future<void> getArchivePayment() async {
try {
isLoading = true;
update();
var res = await CRUD().getWallet(
link: AppLink.getPassengerWalletArchive,
payload: {'passenger_id': box.read(BoxName.passengerID)},
);
if (res != 'failure') {
archive = jsonDecode(res)['message'];
} else {
MyDialog().getDialog('No wallet record found'.tr, '', () {
Get.back();
Get.back();
});
}
} catch (e) {
// MyDialog().getDialog('An error occurred'.tr, '', () {
// Get.back();
// });
} finally {
isLoading = false;
update();
}
}
@override
void onInit() {
getArchivePayment();
super.onInit();
}
}

View File

@@ -0,0 +1,768 @@
import 'dart:convert';
import 'package:siro_rider/constant/api_key.dart';
import 'package:siro_rider/constant/style.dart';
import 'package:siro_rider/controller/firebase/firbase_messge.dart';
import 'package:siro_rider/controller/payment/paymob/paymob_response.dart';
import 'package:siro_rider/views/home/map_page_passenger.dart';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:local_auth/local_auth.dart';
import 'package:siro_rider/controller/home/map/ride_lifecycle_controller.dart';
import 'package:siro_rider/controller/home/map/ride_state.dart';
import 'package:siro_rider/controller/home/map/ride_state.dart';
import 'package:webview_flutter/webview_flutter.dart';
import '../../constant/box_name.dart';
import '../../constant/colors.dart';
import '../../constant/info.dart';
import '../../constant/links.dart';
import '../../main.dart';
import '../../print.dart';
import '../firebase/notification_service.dart';
import '../functions/crud.dart';
import '../functions/encrypt_decrypt.dart';
import '../functions/toast.dart';
import 'paymob/e_cash_screen.dart';
class PaymentController extends GetxController {
bool isLoading = false;
bool isWalletChecked = true;
bool isCashChecked = false;
bool isWalletFound = false;
bool isPromoSheetDialogue = false;
final formKey = GlobalKey<FormState>();
final promo = TextEditingController();
final walletphoneController = TextEditingController();
double totalPassenger = Get.find<RideLifecycleController>().totalPassenger;
int? selectedAmount = 0;
List<dynamic> totalPassengerWalletDetails = [];
String passengerTotalWalletAmount = '';
String ip = '1';
DateTime now = DateTime.now();
late int timestamp;
void updateSelectedAmount(int value) {
selectedAmount = value;
update();
}
void changePromoSheetDialogue() {
isPromoSheetDialogue = !isPromoSheetDialogue;
update();
}
getPassengerWallet() async {
isLoading = true;
update();
await CRUD().getWallet(
link: AppLink.getWalletByPassenger,
payload: {'passenger_id': box.read(BoxName.passengerID)}).then((value) {
box.write(BoxName.passengerWalletTotal,
jsonDecode(value)['message'][0]['total'].toString());
});
isLoading = false;
update();
}
String paymentToken = '';
Future<String> generateTokenPassenger(String amount) async {
var res =
await CRUD().post(link: AppLink.addPaymentTokenPassenger, payload: {
'passengerId': box.read(BoxName.passengerID).toString(),
'amount': amount.toString(),
});
var d = jsonDecode(res);
return d['message'];
}
Future<String> generateTokenDriver(String amount) async {
var res = await CRUD().post(link: AppLink.addPaymentTokenDriver, payload: {
'driverID': Get.find<RideLifecycleController>().driverId,
'amount': amount.toString(),
});
var d = jsonDecode(res);
return d['message'];
}
Future addSeferWallet(String paymentMethod, point) async {
var seferToken = await generateTokenPassenger(point);
await CRUD().postWallet(link: AppLink.addSeferWallet, payload: {
'amount': point.toString(),
'paymentMethod': paymentMethod,
'passengerId': box.read(BoxName.passengerID).toString(),
'token': seferToken,
'driverId': 'passenger',
});
}
Future addPassengersWallet(String point) async {
var token = await generateTokenPassenger(point);
await CRUD().postWallet(link: AppLink.addPassengersWallet, payload: {
'passenger_id': box.read(BoxName.passengerID).toString(),
'balance': point,
'token': token,
});
}
payToDriverForCancelAfterAppliedAndHeNearYou(String rideId) async {
{
double costOfWaiting5Minute = box.read(BoxName.countryCode) == 'Egypt'
? (4 * .08) + (5 * 1)
// 4 indicate foe 4 km ditance from driver start move to passenger
: (4 * .06) + (5 * .06); //for Eygpt other like jordan .06 per minute
var paymentTokenWait =
await generateTokenDriver(costOfWaiting5Minute.toString());
var res =
await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
'rideId': rideId,
'amount': costOfWaiting5Minute.toString(),
'payment_method': 'cancel-from-near',
'passengerID': box.read(BoxName.passengerID).toString(),
'token': paymentTokenWait,
'driverID': Get.find<RideLifecycleController>().driverId.toString(),
});
var paymentTokenWait1 =
await generateTokenDriver(costOfWaiting5Minute.toString());
var res1 = await CRUD()
.postWallet(link: AppLink.addDriversWalletPoints, payload: {
'paymentID': 'rideId$rideId',
'amount': (costOfWaiting5Minute).toStringAsFixed(0),
'paymentMethod': 'cancel-from-near',
'token': paymentTokenWait1,
'driverID': Get.find<RideLifecycleController>().driverId.toString(),
});
if (res != 'failure') {
// Get.find<FirebaseMessagesController>().sendNotificationToDriverMAP(
// 'Cancel',
// 'Trip Cancelled. The cost of the trip will be added to your wallet.'
// .tr,
// Get.find<RideLifecycleController>().driverToken,
// [],
// 'cancel',
// );
await NotificationService.sendNotification(
category: 'Cancel',
target: Get.find<RideLifecycleController>().driverToken.toString(),
title: 'Cancel'.tr,
body:
'Trip Cancelled. The cost of the trip will be added to your wallet.'
.tr,
isTopic: false, // Important: this is a token
tone: 'cancel',
driverList: [],
);
}
var paymentTokenWaitPassenger1 =
await generateTokenPassenger((costOfWaiting5Minute * -1).toString());
await CRUD().post(link: AppLink.addPassengersWallet, payload: {
'passenger_id': box.read(BoxName.passengerID).toString(),
'balance': (costOfWaiting5Minute * -1).toString(),
'token': paymentTokenWaitPassenger1,
});
Get.offAll(const MapPagePassenger());
}
}
addPassengerWallet() async {
isLoading = true;
update();
await addSeferWallet('visa-in', selectedAmount.toString());
await addPassengersWallet(selectedAmount == 100
? '100'
: selectedAmount == 200
? '215'
: selectedAmount == 400
? '450'
: selectedAmount == 1000
? '1140'
: '0');
// getPassengerWallet();
isLoading = false;
update();
}
void onChangedPaymentMethodWallet(bool? value) {
if (box.read(BoxName.passengerWalletTotal) == null ||
double.parse(box.read(BoxName.passengerWalletTotal).toString()) <
totalPassenger) {
isWalletChecked = false;
isWalletChecked ? isCashChecked = true : isCashChecked = true;
update();
} else {
isWalletChecked = !isWalletChecked;
isWalletChecked ? isCashChecked = false : isCashChecked = true;
update();
}
}
void onChangedPaymentMethodCash(bool? value) {
if (box.read(BoxName.passengerWalletTotal) == null ||
double.parse(box.read(BoxName.passengerWalletTotal)) < totalPassenger) {
isWalletChecked = false;
isCashChecked = !isCashChecked;
isCashChecked ? isWalletChecked = false : isWalletChecked = false;
update();
} else {
isCashChecked = !isCashChecked;
isCashChecked ? isWalletChecked = false : isWalletChecked = true;
update();
}
}
void applyPromoCodeToPassenger() async {
//TAWJIHI
CRUD().get(link: AppLink.getPassengersPromo, payload: {
'promo_code': promo.text,
}).then((value) {
var decod = jsonDecode(value);
if (decod["status"] == "success") {
var firstElement = decod["message"][0];
totalPassenger = totalPassenger -
(totalPassenger * int.parse(firstElement['amount']));
Get.find<RideLifecycleController>().promoTaken = true;
update();
}
});
}
// 'https://accept.paymob.com/unifiedcheckout/?publicKey=egy_pk_live_mbjDC9Ni6FSHKmsz8sOHiVk2xd7oWRve&clientSecret=egy_sk_live_c0904e9cf04506ae64f818d4e075b4a957e3713fdf7a22cb7da30a29e72442b5'
// أضف هذا الرابط إلى ملف AppLink الخاص بك
// هذه هي الدالة الجديدة التي ستستخدمها لبدء الدفع
Future<void> payWithEcash(BuildContext context, String amount) async {
try {
// 1. يمكنك استخدام نفس طريقة التحقق بالبصمة إذا أردت
bool isAvailable = await LocalAuthentication().isDeviceSupported();
if (isAvailable) {
bool didAuthenticate = await LocalAuthentication().authenticate(
localizedReason: 'Use Touch ID or Face ID to confirm payment',
);
if (didAuthenticate) {
// 2. استدعاء الـ Endpoint الجديد على السيرفر الخاص بك
var res = await CRUD().postWallet(
link: AppLink.payWithEcashPassenger,
payload: {
// ✅ أرسل البيانات التي يحتاجها السيرفر الخاص بـ ecash
"amount": amount,
"passengerId": box.read(BoxName.passengerID),
},
);
// 3. التأكد من أن السيرفر أعاد رابط الدفع بنجاح
if (res != null &&
res['status'] == 'success' &&
res['message'] != null) {
final String paymentUrl = res['message'];
// 4. الانتقال إلى شاشة الدفع الجديدة الخاصة بـ ecash
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
EcashPaymentScreen(paymentUrl: paymentUrl),
),
);
} else {
// عرض رسالة خطأ في حال فشل السيرفر في إنشاء الرابط
Get.defaultDialog(
title: 'Error'.tr,
content: Text(
'Failed to initiate payment. Please try again.'.tr,
style: AppStyle.title,
),
);
}
}
}
} catch (e) {
Get.defaultDialog(
title: 'Error'.tr,
content: Text(
'An error occurred during the payment process.'.tr,
style: AppStyle.title,
),
);
}
}
// Future<void> payWithEcashDriver(BuildContext context, String amount) async {
// try {
// // يمكنك استخدام نفس طريقة التحقق بالبصمة إذا أردت
// bool isAvailable = await LocalAuthentication().isDeviceSupported();
// if (isAvailable) {
// bool didAuthenticate = await LocalAuthentication().authenticate(
// localizedReason: 'Use Touch ID or Face ID to confirm payment'.tr,
// );
// if (didAuthenticate) {
// // استدعاء الـ Endpoint الجديد على السيرفر الخاص بك
// var res = await CRUD().postWallet(
// link: AppLink.payWithEcashPassenger,
// // link:
// // 'https://wl.tripz-egypt.com/v1/main/ride/ecash/driver/payWithEcash.php',
// payload: {
// // أرسل البيانات التي يحتاجها السيرفر
// "amount": amount,
// // "driverId": box.read(BoxName.driverID), // تأكد من وجود هذا المتغير
// "passengerId":
// box.read(BoxName.passengerID), // تأكد من وجود هذا المتغير
// },
// );
// // التأكد من أن السيرفر أعاد رابط الدفع بنجاح
// if (res != null &&
// res['status'] == 'success' &&
// res['message'] != null) {
// final String paymentUrl = res['message'];
// // الانتقال إلى شاشة الدفع الجديدة الخاصة بـ ecash للسائق
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) =>
// EcashDriverPaymentScreen(paymentUrl: paymentUrl),
// ),
// );
// } else {
// // عرض رسالة خطأ في حال فشل السيرفر في إنشاء الرابط
// Get.defaultDialog(
// title: 'Error'.tr,
// content: Text(
// 'Failed to initiate payment. Please try again.'.tr,
// style: AppStyle.title,
// ),
// );
// }
// }
// }
// } catch (e) {
// Get.defaultDialog(
// title: 'Error'.tr,
// content: Text(
// 'An error occurred during the payment process.'.tr,
// style: AppStyle.title,
// ),
// );
// }
// }
/// شاشة جديدة ومبسطة خاصة بدفع السائقين عبر ecash
// Future<void> payWithMTNWallet(
// BuildContext context, String amount, String currency) async {
// // خزن سياق علوي آمن من البداية
// final BuildContext safeContext =
// Get.overlayContext ?? Get.context ?? context;
// // سبينر تحميل
// if (!(Get.isDialogOpen ?? false)) {
// Get.dialog(const Center(child: CircularProgressIndicator()),
// barrierDismissible: false);
// }
// try {
// final phone = box.read(BoxName.phoneWallet) as String;
// final passengerID = box.read(BoxName.passengerID).toString();
// final formattedAmount = double.parse(amount).toStringAsFixed(0);
// Log.print("🚀 بدء عملية دفع MTN");
// Log.print(
// "📦 Payload: passengerID: $passengerID, amount: $formattedAmount, phone: $phone");
// // التحقق بالبصمة (اختياري) + حماية من الـ await
// final localAuth = LocalAuthentication();
// final isAuthSupported = await localAuth.isDeviceSupported();
// if (isAuthSupported) {
// final didAuth = await localAuth.authenticate(
// localizedReason: 'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع',
// );
// if (!didAuth) {
// if (Get.isDialogOpen == true) Get.back();
// Log.print("❌ المستخدم لم يؤكد بالبصمة/الوجه");
// return;
// }
// }
// // 1) بدء الدفع
// final responseData = await CRUD().postWalletMtn(
// link: AppLink.payWithMTNStart,
// payload: {
// "amount": formattedAmount,
// "passengerId": passengerID,
// "phone": phone,
// "lang": box.read(BoxName.lang) ?? 'ar',
// },
// );
// // Log.print("✅ استجابة الخادم (mtn_start_payment.php):");
// // Log.print(responseData);
// Log.print('responseData: ${responseData}');
// // فحص الاستجابة بقوة
// late final Map<String, dynamic> startRes;
// if (responseData is Map<String, dynamic>) {
// startRes = responseData;
// } else if (responseData is String) {
// startRes = json.decode(responseData) as Map<String, dynamic>;
// } else {
// throw Exception("تم استلام نوع بيانات غير متوقع من الخادم.");
// }
// if (startRes['status'] != 'success') {
// final errorMsg = startRes['message']['Error']?.toString().tr ??
// "فشل بدء عملية الدفع. حاول مرة أخرى.";
// throw Exception(errorMsg);
// }
// final messageData = startRes["message"] as Map<String, dynamic>;
// final invoiceNumber = messageData["invoiceNumber"].toString();
// final operationNumber = messageData["operationNumber"].toString();
// final guid = messageData["guid"].toString();
// // Log.print(
// // "📄 invoiceNumber: $invoiceNumber, 🔢 operationNumber: $operationNumber, 🧭 guid: $guid");
// // أغلق السبينر قبل إظهار حوار OTP
// if (Get.isDialogOpen == true) Get.back();
// // 2) إدخال OTP بـ Get.defaultDialog (لا يستخدم context قابل للتلف)
// String otpInput = "";
// await Get.defaultDialog(
// title: "أدخل كود التحقق",
// barrierDismissible: false,
// content: TextField(
// keyboardType: TextInputType.number,
// decoration: const InputDecoration(hintText: "كود OTP"),
// onChanged: (v) => otpInput = v,
// ),
// confirm: TextButton(
// onPressed: () {
// if (otpInput.isEmpty ||
// otpInput.length < 4 ||
// otpInput.length > 8) {
// Get.snackbar("تنبيه", "أدخل كود OTP صحيح (48 أرقام)");
// return;
// }
// Get.back(result: otpInput);
// },
// child: const Text("تأكيد"),
// ),
// cancel: TextButton(
// onPressed: () => Get.back(result: null),
// child: const Text("إلغاء"),
// ),
// ).then((res) => otpInput = (res ?? "") as String);
// if (otpInput.isEmpty) {
// Log.print("❌ لم يتم إدخال OTP");
// return;
// }
// Log.print("🔐 تم إدخال OTP: $otpInput");
// // سبينر أثناء التأكيد
// Get.dialog(const Center(child: CircularProgressIndicator()),
// barrierDismissible: false);
// // 3) تأكيد الدفع
// final confirmRes = await CRUD().postWalletMtn(
// link: AppLink.payWithMTNConfirm,
// payload: {
// "invoiceNumber": invoiceNumber,
// "operationNumber": operationNumber,
// "guid": guid,
// "otp": otpInput,
// "phone": phone,
// "lang": box.read(BoxName.lang) ?? 'ar',
// },
// );
// if (Get.isDialogOpen == true) Get.back();
// // Log.print("✅ استجابة mtn_confirm.php:");
// // Log.print('confirmRes: ${confirmRes}');
// final ok = (confirmRes is Map && confirmRes['status'] == 'success');
// if (ok) {
// Get.defaultDialog(
// title: "✅ نجاح",
// content: const Text("تمت عملية الدفع وإضافة الرصيد إلى محفظتك."),
// );
// await getPassengerWallet();
// } else {
// final errorMsg = (confirmRes['message']['message']?.toString()) ??
// "فشل في تأكيد الدفع";
// Get.defaultDialog(title: "❌ فشل", content: Text(errorMsg.tr));
// }
// } catch (e, s) {
// Log.print("🔥 خطأ أثناء الدفع عبر MTN:");
// Log.print(e);
// Log.print(s);
// if (Get.isDialogOpen == true) Get.back();
// Get.defaultDialog(
// title: 'حدث خطأ',
// content: Text(e.toString().replaceFirst("Exception: ", "")),
// );
// }
// }
Future<void> payWithSyriaTelWallet(String amount, String currency) async {
// helper لفتح لودينغ بأمان
Future<void> _showLoading() async {
if (!(Get.isDialogOpen ?? false)) {
Get.dialog(const Center(child: CircularProgressIndicator()),
barrierDismissible: false);
}
}
// helper لإغلاق أي حوار مفتوح
void _closeAnyDialog() {
if (Get.isDialogOpen ?? false) {
Get.back();
}
}
await _showLoading();
try {
final phone = box.read(BoxName.phoneWallet) as String;
final passengerId = box.read(BoxName.passengerID).toString();
final formattedAmount = double.parse(amount).toStringAsFixed(0);
Log.print("🚀 Syriatel payment start");
Log.print(
"📦 Payload => passengerId:$passengerId amount:$formattedAmount phone:$phone");
// مصادقة حيوية (اختياري)
final auth = LocalAuthentication();
if (await auth.isDeviceSupported()) {
final ok = await auth.authenticate(
localizedReason: 'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع',
);
if (!ok) {
_closeAnyDialog();
Log.print("❌ User did not authenticate");
return;
}
}
// 1) بدء عملية الدفع
final startRaw = await CRUD().postWalletMtn(
link: AppLink.payWithSyriatelStart,
payload: {
"amount": formattedAmount,
"passengerId": passengerId,
"phone": phone,
"lang": box.read(BoxName.lang) ?? 'ar',
},
);
Log.print("✅ Server response (start): $startRaw");
// تحويل الاستجابة إلى Map
late final Map<String, dynamic> startRes;
if (startRaw is Map<String, dynamic>) {
startRes = startRaw;
} else if (startRaw is String) {
startRes = json.decode(startRaw) as Map<String, dynamic>;
} else {
throw Exception("Unexpected start response type");
}
if (startRes['status'] != 'success') {
final msg =
(startRes['message'] ?? 'Failed to start payment').toString();
throw Exception(msg);
}
final messageData = startRes['message'] as Map<String, dynamic>;
final transactionID = messageData['transactionID'].toString();
Log.print("📄 transactionID: $transactionID");
//
// 2) اطلب من المستخدم إدخال OTP عبر Get.dialog (بدون context)
_closeAnyDialog(); // أغلق اللودينغ أولاً
final otpController = TextEditingController();
final otp = await Get.dialog<String>(
AlertDialog(
title: const Text("أدخل كود التحقق"),
content: TextField(
controller: otpController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(hintText: "كود OTP"),
),
actions: [
TextButton(
child: const Text("تأكيد"),
onPressed: () => Get.back(result: otpController.text.trim()),
),
TextButton(
child: const Text("إلغاء"),
onPressed: () => Get.back(result: null),
),
],
),
barrierDismissible: false,
);
if (otp == null || otp.isEmpty) {
Log.print("❌ OTP not provided");
return;
}
Log.print("🔐 OTP: $otp");
await _showLoading();
// 3) تأكيد الدفع
final confirmRaw = await CRUD().postWallet(
link: AppLink.payWithSyriatelConfirm,
payload: {
"transactionID": transactionID,
"otp": otp,
},
);
_closeAnyDialog(); // أغلق اللودينغ
Log.print("✅ Response (confirm): $confirmRaw");
late final Map<String, dynamic> confirmRes;
if (confirmRaw is Map<String, dynamic>) {
confirmRes = confirmRaw;
} else if (confirmRaw is String) {
confirmRes = json.decode(confirmRaw) as Map<String, dynamic>;
} else {
throw Exception("Unexpected confirm response type");
}
if (confirmRes['status'] == 'success') {
Get.defaultDialog(
title: "✅ نجاح",
content: const Text("تمت عملية الدفع وإضافة الرصيد إلى محفظتك."),
);
} else {
final msg = (confirmRes['message'] ?? 'فشل في تأكيد الدفع').toString();
Get.defaultDialog(
title: "❌ فشل",
content: Text(msg),
);
}
} catch (e, s) {
Log.print("🔥 Error during Syriatel Wallet payment:\n$e\n$s");
_closeAnyDialog();
Get.defaultDialog(
title: 'حدث خطأ',
content: Text(e.toString().replaceFirst("Exception: ", "")),
);
}
}
@override
void onInit() {
timestamp = now.millisecondsSinceEpoch;
if (box.read(BoxName.passengerWalletTotal) == null) {
box.write(BoxName.passengerWalletTotal, '0');
}
getPassengerWallet();
final localAuth = LocalAuthentication();
super.onInit();
}
}
class EcashDriverPaymentScreen extends StatefulWidget {
final String paymentUrl;
const EcashDriverPaymentScreen({required this.paymentUrl, Key? key})
: super(key: key);
@override
State<EcashDriverPaymentScreen> createState() =>
_EcashDriverPaymentScreenState();
}
class _EcashDriverPaymentScreenState extends State<EcashDriverPaymentScreen> {
late final WebViewController _controller;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(NavigationDelegate(
onPageFinished: (url) async {
Log.print('Ecash Driver WebView URL Finished: $url');
await Get.find<PaymentController>().getPassengerWallet();
// هنا نتحقق فقط من أن المستخدم عاد إلى صفحة النجاح
// لا حاجة لاستدعاء أي API هنا، فالـ Webhook يقوم بكل العمل
if (url.contains("success.php")) {
showProcessingDialog();
}
},
))
..loadRequest(Uri.parse(widget.paymentUrl));
}
// دالة لعرض رسالة "العملية قيد المعالجة"
void showProcessingDialog() {
showDialog(
barrierDismissible: false,
context: Get.context!,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
title: Row(
children: [
Icon(Icons.check_circle, color: Colors.green),
const SizedBox(width: 8),
Text(
"Payment Successful".tr,
style: TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
],
),
content: Text(
"Your payment is being processed and your wallet will be updated shortly."
.tr,
style: const TextStyle(fontSize: 16),
),
actions: [
TextButton(
onPressed: () {
// أغلق مربع الحوار، ثم أغلق شاشة الدفع
Navigator.pop(context); // Close the dialog
Navigator.pop(context); // Close the payment screen
},
style: TextButton.styleFrom(
backgroundColor: Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
child: Text(
"OK".tr,
style: const TextStyle(color: Colors.white),
),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Complete Payment'.tr)),
body: WebViewWidget(controller: _controller),
);
}
}

View File

@@ -0,0 +1,158 @@
import 'package:siro_rider/constant/box_name.dart';
import 'package:dio/dio.dart' as dio;
import 'package:dio/dio.dart';
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../constant/api_key.dart';
import '../../main.dart';
import '../../print.dart';
import '../functions/encrypt_decrypt.dart';
class PaymobManager extends GetxController {
String authanticationToken1 = "";
String orderId1 = "";
Future<String> getPaymentKey(int amount, String currency) async {
try {
String authanticationToken = await _getAuthanticationToken();
int orderId = await _getOrderId(
authanticationToken: authanticationToken,
amount: (100 * amount).toString(),
currency: currency,
);
String paymentKey = await _getPaymentKey(
authanticationToken: authanticationToken,
amount: (100 * amount).toString(),
currency: currency,
orderId: orderId.toString(),
);
authanticationToken1 = authanticationToken.toString();
orderId1 = orderId.toString();
update();
return paymentKey;
} catch (e) {
throw Exception();
}
}
Future<void> payWithPayMob(int amount, String currency) async {
// 1. Fetch Payment Key (Assuming PaymobManager is a custom class)
String paymentToken;
try {
paymentToken = await PaymobManager().getPaymentKey(amount, currency);
} on Exception catch (e) {
// Handle errors gracefully, e.g., display error message to user
return;
}
// 2. Prepare Payment Data Payload
final Map<String, dynamic> data = {
"source": {
"identifier": box.read(BoxName.phone), //"01010101010"
"subtype": "WALLET",
},
"payment_token": paymentToken,
};
// 3. Make Payment Request using Dio
final dio = Dio();
try {
final response = await dio.post(
'https://accept.paymobsolutions.com/api/acceptance/payments/pay',
data: data,
);
// 4. Handle Payment Response
if (response.statusCode == 200) {
final paymentData = response.data; // Assuming JSON response
// Navigate to success screen or display success message
launchUrl(Uri.parse(paymentData['iframe_redirection_url']));
} else {
// Payment failed: Handle errors (e.g., display error message)
}
} on DioError catch (e) {
// Handle network or Dio-related errors
}
}
Future<String> _getStatusAfterPaid() async {
final dio.Response response = await Dio().post(
"https://accept.paymob.com/api/ecommerce/orders/transaction_inquiry",
data: {
"auth_token": authanticationToken1,
"merchant_order_id": "970960",
"order_id": orderId1
});
return response.data["success"];
}
Future<String> _getAuthanticationToken() async {
final dio.Response response =
await Dio().post("https://accept.paymob.com/api/auth/tokens", data: {
"api_key": AK.payMobApikey,
'username': AK.usernamePayMob,
"password": AK.passwordPayMob,
});
Log.print('token: ${response}');
return response.data["token"];
}
Future<int> _getOrderId({
required String authanticationToken,
required String amount,
required String currency,
}) async {
final dio.Response response = await Dio()
.post("https://accept.paymob.com/api/ecommerce/orders", data: {
"auth_token": authanticationToken,
"amount_cents": amount,
"currency": currency,
"delivery_needed": "false",
"items": [],
});
Log.print('id: ${response}');
return response.data["id"];
}
Future<String> _getPaymentKey({
required String authanticationToken,
required String orderId,
required String amount,
required String currency,
}) async {
final dio.Response response = await Dio()
.post("https://accept.paymob.com/api/acceptance/payment_keys", data: {
"expiration": 200,
"auth_token": authanticationToken.toString(),
"order_id": orderId.toString(),
"integration_id":
4601103, ////todo wallet or online card int.parse(AK.integrationIdPayMob),
"lock_order_when_paid": "false",
"amount_cents": amount,
"currency": currency,
"billing_data": {
"first_name":
(box.read(BoxName.name).toString().split(' ')[0]).toString(),
"last_name":
(box.read(BoxName.name).toString().split(' ')[1]).toString(),
"email": (box.read(BoxName.email)),
"phone_number": (box.read(BoxName.phone)),
"apartment": "NA",
"floor": "NA",
"street": "NA",
"building": "NA",
"shipping_method": "NA",
"postal_code": "NA",
"city": "NA",
"country": box.read(BoxName.countryCode),
"state": "NA"
},
});
Log.print('token: ${response}');
return response.data["token"];
}
}

View File

@@ -0,0 +1,100 @@
import 'package:siro_rider/print.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:webview_flutter/webview_flutter.dart';
// ✅ شاشة جديدة ومبسطة خاصة بـ ecash
class EcashPaymentScreen extends StatefulWidget {
final String paymentUrl;
const EcashPaymentScreen({required this.paymentUrl, Key? key})
: super(key: key);
@override
State<EcashPaymentScreen> createState() => _EcashPaymentScreenState();
}
class _EcashPaymentScreenState extends State<EcashPaymentScreen> {
late final WebViewController _controller;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(NavigationDelegate(
onPageFinished: (url) {
Log.print('Ecash WebView URL Finished: $url');
// ✅ هنا نتحقق فقط من أن المستخدم عاد إلى صفحة النجاح
// هذه الصفحة هي التي حددناها في `APP_REDIRECT_URL_SUCCESS` في ملف PHP
if (url.contains("success.php")) {
// لا نستدعي أي API هنا. الـ Webhook على السيرفر يقوم بكل العمل.
// فقط نعرض للمستخدم رسالة بأن العملية قيد المراجعة ونغلق الشاشة.
showProcessingDialog();
}
},
))
..loadRequest(Uri.parse(widget.paymentUrl));
}
// دالة لعرض رسالة "العملية قيد المعالجة"
void showProcessingDialog() {
showDialog(
barrierDismissible: false,
context: Get.context!,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
title: Row(
children: [
Icon(Icons.check_circle, color: Colors.green),
const SizedBox(width: 8),
Text(
"Payment Successful".tr,
style: TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
],
),
content: Text(
"Your payment is being processed and your wallet will be updated shortly."
.tr,
style: const TextStyle(fontSize: 16),
),
actions: [
TextButton(
onPressed: () {
// أغلق مربع الحوار، ثم أغلق شاشة الدفع
Navigator.pop(context); // Close the dialog
Navigator.pop(context); // Close the payment screen
},
style: TextButton.styleFrom(
backgroundColor: Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
child: Text(
"OK".tr,
style: const TextStyle(color: Colors.white),
),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Complete Payment'.tr)),
body: WebViewWidget(controller: _controller),
);
}
}

View File

@@ -0,0 +1,487 @@
import 'dart:convert';
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/constant/links.dart';
import 'package:siro_rider/controller/functions/crud.dart';
import 'package:siro_rider/main.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:http/http.dart' as http;
import '../../../print.dart';
import '../../functions/encrypt_decrypt.dart';
class PaymobResponse {
bool success;
String? transactionID;
String? responseCode;
String? message;
PaymobResponse({
this.transactionID,
required this.success,
this.responseCode,
this.message,
});
factory PaymobResponse.fromJson(Map<String, dynamic> json) {
return PaymobResponse(
success: json['success'] == 'true',
transactionID: json['id'],
message: json['message'],
responseCode: json['txn_response_code'],
);
}
}
class PaymobPayment {
static PaymobPayment instance = PaymobPayment();
bool _isInitialized = false;
final Dio _dio = Dio();
final _baseURL = 'https://accept.paymob.com/api/';
late String _apiKey;
late int _integrationID;
late int _iFrameID;
late String _iFrameURL;
late int _userTokenExpiration;
/// Initializing PaymobPayment instance.
Future<bool> initialize({
/// It is a unique identifier for the merchant which used to authenticate your requests calling any of Accept's API.
/// from dashboard Select Settings -> Account Info -> API Key
required String apiKey,
/// from dashboard Select Developers -> Payment Integrations -> Online Card ID
required int integrationID,
/// from paymob Select Developers -> iframes
required int iFrameID,
/// The expiration time of this payment token in seconds. (The maximum is 3600 seconds which is an hour)
int userTokenExpiration = 300,
}) async {
if (_isInitialized) {
return true;
}
_dio.options.baseUrl = _baseURL;
_dio.options.validateStatus = (status) => true;
_apiKey = apiKey;
_integrationID = integrationID;
_iFrameID = iFrameID;
_iFrameURL =
'https://accept.paymobsolutions.com/api/acceptance/iframes/$_iFrameID?payment_token=';
_isInitialized = true;
_userTokenExpiration = userTokenExpiration;
return _isInitialized;
}
/// Get authentication token, which is valid for one hour from the creation time.
Future<String> _getAuthToken() async {
try {
final response = await _dio.post(
'auth/tokens',
data: {
'api_key': _apiKey,
},
);
return response.data['token'];
} catch (e) {
rethrow;
}
}
/// At this step, you will register an order to Accept's database, so that you can pay for it later using a transaction
Future<int> _addOrder({
required String authToken,
required String currency,
required String amount,
required List items,
}) async {
try {
final response = await _dio.post(
'ecommerce/orders',
data: {
"auth_token": authToken,
"delivery_needed": "false",
"amount_cents": amount,
"currency": currency,
"items": items,
},
);
return response.data['id'];
} catch (e) {
rethrow;
}
}
/// At this step, you will obtain a payment_key token. This key will be used to authenticate your payment request. It will be also used for verifying your transaction request metadata.
Future<String> _getPurchaseToken({
required String authToken,
required String currency,
required int orderID,
required String amount,
required PaymobBillingData billingData,
}) async {
final response = await _dio.post(
'acceptance/payment_keys',
data: {
"auth_token": authToken,
"amount_cents": amount,
"expiration": _userTokenExpiration,
"order_id": orderID,
"billing_data": billingData,
"currency": currency,
"integration_id": _integrationID,
"lock_order_when_paid": "false"
},
);
final message = response.data['message'];
if (message != null) {
throw Exception(message);
}
return response.data['token'];
}
/// Proceed to pay with only calling this function.
/// Opens a WebView at Paymob redirectedURL to accept user payment info.
Future<PaymobResponse?> pay(
{
/// BuildContext for navigation to WebView
required BuildContext context,
/// Which Currency you would pay in.
required String currency,
/// Payment amount in cents EX: 20000 is an 200 EGP
required String amountInCents,
/// Optional Callback if you can use return result of pay function or use this callback
void Function(PaymobResponse response)? onPayment,
/// list of json objects contains the contents of the purchase.
List? items,
/// The billing data related to the customer related to this payment.
PaymobBillingData? billingData}) async {
if (!_isInitialized) {
throw Exception(
'PaymobPayment is not initialized call:`PaymobPayment.instance.initialize`');
}
final authToken = await _getAuthToken();
final orderID = await _addOrder(
authToken: authToken,
currency: currency,
amount: amountInCents,
items: items ?? [],
);
final purchaseToken = await _getPurchaseToken(
authToken: authToken,
currency: currency,
orderID: orderID,
amount: amountInCents,
billingData: billingData ?? PaymobBillingData(),
);
if (context.mounted) {
final response = await PaymobIFrame.show(
context: context,
redirectURL: _iFrameURL + purchaseToken,
onPayment: onPayment,
);
return response;
}
return null;
} //51624
}
class PaymobBillingData {
String? email;
String? firstName;
String? lastName;
String? phoneNumber;
String? apartment;
String? floor;
String? street;
String? building;
String? postalCode;
String? city;
String? state;
String? country;
String? shippingMethod;
PaymobBillingData({
this.email,
this.firstName,
this.lastName,
this.phoneNumber,
this.apartment,
this.floor,
this.street,
this.building,
this.postalCode,
this.city,
this.state,
this.country,
this.shippingMethod,
});
Map<String, dynamic> toJson() {
return {
"email": box.read(BoxName.email) ?? box.read(BoxName.emailDriver),
"first_name":
(box.read(BoxName.name).toString().split(' ')[0]).toString(),
"last_name": (box.read(BoxName.name).toString().split(' ')[1]).toString(),
"phone_number": (box.read(BoxName.phone)),
"apartment": apartment ?? "NA",
"floor": floor ?? "NA",
"building": building ?? "NA",
"street": street ?? "NA",
"postal_code": postalCode ?? "NA",
"city": city ?? "NA",
"state": state ?? "NA",
"country": country ?? "NA",
"shipping_method": box.read(BoxName.passengerID) ?? "NA",
};
}
}
class PaymobIFrame extends StatefulWidget {
const PaymobIFrame({
Key? key,
required this.redirectURL,
this.onPayment,
}) : super(key: key);
final String redirectURL;
final void Function(PaymobResponse)? onPayment;
static Future<PaymobResponse?> show({
required BuildContext context,
required String redirectURL,
void Function(PaymobResponse)? onPayment,
}) =>
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return PaymobIFrame(
onPayment: onPayment,
redirectURL: redirectURL,
);
},
),
);
@override
State<PaymobIFrame> createState() => _PaymobIFrameState();
}
class _PaymobIFrameState extends State<PaymobIFrame> {
WebViewController? controller;
@override
void initState() {
controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onNavigationRequest: (NavigationRequest request) {
if (request.url.contains('txn_response_code') &&
request.url.contains('success') &&
request.url.contains('id')) {
final params = _getParamFromURL(request.url);
final response = PaymobResponse.fromJson(params);
if (widget.onPayment != null) {
widget.onPayment!(response);
}
Navigator.pop(context, response);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
),
)
..loadRequest(Uri.parse(widget.redirectURL));
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: controller == null
? const Center(
child: CircularProgressIndicator.adaptive(),
)
: SafeArea(
child: WebViewWidget(
controller: controller!,
),
),
);
}
Map<String, dynamic> _getParamFromURL(String url) {
final uri = Uri.parse(url);
Map<String, dynamic> data = {};
uri.queryParameters.forEach((key, value) {
data[key] = value;
});
return data;
}
}
class PaymentScreen extends StatefulWidget {
final String iframeUrl;
const PaymentScreen({required this.iframeUrl, Key? key}) : super(key: key);
@override
State<PaymentScreen> createState() => _PaymentScreenState();
}
class _PaymentScreenState extends State<PaymentScreen> {
late final WebViewController _controller;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(NavigationDelegate(
onPageFinished: (url) {
Log.print('url onPageFinished : ${url}');
if (url.contains("success")) {
_fetchPaymentStatus(); // ✅ استدعاء الويب هوك بعد نجاح الدفع
} else if (url.contains("failed")) {
showCustomDialog(
title: "Error".tr,
message: 'Payment Failed'.tr, // يتم جلب رسالة الخطأ من الخادم
isSuccess: false,
);
}
},
))
..loadRequest(Uri.parse(widget.iframeUrl));
}
// ✅ استدعاء الويب هوك بعد انتهاء الدفع
Future<void> _fetchPaymentStatus() async {
final String userId = (box.read(BoxName.phoneWallet)); // ضع user_id الحقيقي
final String apiUrl = AppLink.paymetVerifyPassenger;
try {
final response = await CRUD().getWallet(link: apiUrl, payload: {
'user_id': userId,
'passengerId': box.read(BoxName.passengerID),
'paymentMethod': 'visa-in',
});
if (response != 'failure' && response != 'token_expired') {
try {
final jsonData = jsonDecode(response);
if (jsonData['status'] == 'success') {
// تأكد أن 'message' هو String وليس Map
// final message = jsonData['message'];
showCustomDialog(
title: "Payment Status",
message: jsonData['message'], // يتم جلب الرسالة من الخادم
isSuccess: true,
);
} else {
showCustomDialog(
title: "Error",
message: jsonData['message'], // يتم جلب رسالة الخطأ من الخادم
isSuccess: false,
);
}
} catch (e) {
showCustomDialog(
title: "Error",
message: response, // يتم جلب رسالة الخطأ من الخادم
isSuccess: false,
);
}
} else {
showCustomDialog(
title: "Error".tr,
message: response, // يتم جلب رسالة الخطأ من الخادم
isSuccess: false,
);
}
} catch (e) {
showCustomDialog(
title: "Error".tr,
message: 'Server error'.tr, // يتم جلب رسالة الخطأ من الخادم
isSuccess: false,
);
}
}
void showCustomDialog({
required String title,
required String message,
required bool isSuccess,
}) {
showDialog(
barrierDismissible: false,
context: Get.context!,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
title: Row(
children: [
Icon(
isSuccess ? Icons.check_circle : Icons.error,
color: isSuccess ? Colors.green : Colors.red,
),
const SizedBox(width: 8),
Text(
title,
style: TextStyle(
color: isSuccess ? Colors.green : Colors.red,
fontWeight: FontWeight.bold,
),
),
],
),
content: Text(
message,
style: const TextStyle(fontSize: 16),
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
Navigator.pop(context);
},
style: TextButton.styleFrom(
backgroundColor: isSuccess ? Colors.green : Colors.red,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
child: Text(
"OK",
style: const TextStyle(color: Colors.white),
),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('إتمام الدفع')),
body: WebViewWidget(controller: _controller),
);
}
}

View File

@@ -0,0 +1,382 @@
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/main.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import '../../../print.dart';
import '../../functions/encrypt_decrypt.dart';
class PaymobResponseWallet {
final bool success;
final String? transactionID;
final String? responseCode;
final String? message;
PaymobResponseWallet({
required this.success,
this.transactionID,
this.responseCode,
this.message,
});
factory PaymobResponseWallet.fromJson(Map<String, dynamic> json) {
return PaymobResponseWallet(
success: json['success'] == 'true',
transactionID: json['id'],
responseCode: json['txn_response_code'],
message: json['data']['message'],
);
}
}
class PaymobPaymentWallet {
static PaymobPaymentWallet instance = PaymobPaymentWallet();
bool _isInitializedWallet = false;
final Dio _dio = Dio();
final _baseURL = 'https://accept.paymob.com/api/';
late String _apiKey;
late int _integrationID;
late int _iFrameID;
late String _iFrameURL;
late int _userTokenExpiration;
/// Initializing PaymobPayment instance.
Future<bool> initialize({
/// It is a unique identifier for the merchant which used to authenticate your requests calling any of Accept's API.
/// from dashboard Select Settings -> Account Info -> API Key
required String apiKey,
/// from dashboard Select Developers -> Payment Integrations -> Online Card ID
required int integrationID,
/// from paymob Select Developers -> iframes
required int iFrameID,
/// The expiration time of this payment token in seconds. (The maximum is 3600 seconds which is an hour)
int userTokenExpiration = 300,
}) async {
if (_isInitializedWallet) {
return true;
}
_dio.options.baseUrl = _baseURL;
_dio.options.validateStatus = (status) => true;
_apiKey = apiKey;
_integrationID = integrationID;
_iFrameID = iFrameID;
_iFrameURL =
'https://accept.paymobsolutions.com/api/acceptance/iframes/$_iFrameID?payment_token=';
_isInitializedWallet = true;
_userTokenExpiration = userTokenExpiration;
return _isInitializedWallet;
}
/// Get authentication token, which is valid for one hour from the creation time.
Future<String> _getAuthToken() async {
try {
final response = await _dio.post(
'auth/tokens',
data: {
'api_key': _apiKey,
},
);
return response.data['token'];
} catch (e) {
rethrow;
}
}
/// At this step, you will register an order to Accept's database, so that you can pay for it later using a transaction
Future<int> _addOrder({
required String authToken,
required String currency,
required String amount,
required List items,
}) async {
try {
final response = await _dio.post(
'ecommerce/orders',
data: {
"auth_token": authToken,
"delivery_needed": "false",
"amount_cents": amount,
"currency": currency,
"items": items,
},
);
return response.data['id'];
} catch (e) {
rethrow;
}
}
/// At this step, you will obtain a payment_key token. This key will be used to authenticate your payment request. It will be also used for verifying your transaction request metadata.
Future<String> _getPurchaseToken({
required String authToken,
required String currency,
required int orderID,
required String amount,
required PaymobBillingDataWallet billingData,
}) async {
final response = await _dio.post(
'acceptance/payment_keys',
data: {
"auth_token": authToken,
"amount_cents": amount,
"expiration": _userTokenExpiration,
"order_id": orderID,
"billing_data": billingData,
"currency": currency,
"integration_id": _integrationID,
"lock_order_when_paid": "false"
},
);
// final message = response.data['message'];
// if (message != null) {
// throw Exception(message);
// }
return response.data['token'];
}
Future<String> _getWalletUrl({
required String paymentToken,
}) async {
final Map<String, dynamic> data = {
"source": {
"identifier": (box.read(BoxName.phoneWallet).toString()),
"subtype": "WALLET",
},
"payment_token": paymentToken,
};
final dio = Dio();
try {
final response = await dio.post(
'https://accept.paymobsolutions.com/api/acceptance/payments/pay',
data: data,
);
// 4. Handle Payment Response
if (response.statusCode == 200) {
final paymentData = response.data; // Assuming JSON response
return paymentData['iframe_redirection_url'];
// Navigate to success screen or display success message
} else {
// Payment failed: Handle errors (e.g., display error message)
}
} on DioError catch (e) {
// Handle network or Dio-related errors
}
return '';
}
/// Proceed to pay with only calling this function.
/// Opens a WebView at Paymob redirectedURL to accept user payment info.
Future<PaymobResponseWallet?> pay(
{
/// BuildContext for navigation to WebView
required BuildContext context,
/// Which Currency you would pay in.
required String currency,
/// Payment amount in cents EX: 20000 is an 200 EGP
required String amountInCents,
/// Optional Callback if you can use return result of pay function or use this callback
void Function(PaymobResponseWallet response)? onPayment,
/// list of json objects contains the contents of the purchase.
List? items,
/// The billing data related to the customer related to this payment.
PaymobBillingDataWallet? billingData}) async {
if (!_isInitializedWallet) {
throw Exception(
'PaymobPayment is not initialized call:`PaymobPayment.instance.initialize`');
}
final authToken = await _getAuthToken();
final orderID = await _addOrder(
authToken: authToken,
currency: currency,
amount: amountInCents,
items: items ?? [],
);
final purchaseToken = await _getPurchaseToken(
authToken: authToken,
currency: currency,
orderID: orderID,
amount: amountInCents,
billingData: billingData ??
PaymobBillingDataWallet(
// email: box.read(BoxName.email) ?? box.read(BoxName.emailDriver),
// firstName: box.read(BoxName.name) ?? box.read(BoxName.nameDriver),
// lastName:
// box.read(BoxName.lastNameDriver) ?? box.read(BoxName.name),
// phoneNumber:
// box.read(BoxName.phone) ?? box.read(BoxName.phoneDriver),
),
);
final urlWallet = await _getWalletUrl(paymentToken: purchaseToken);
Log.print('urlWallet: ${urlWallet}');
if (context.mounted) {
final response = await PaymobIFrameWallet.show(
context: context,
redirectURL: urlWallet,
onPayment: onPayment,
);
return response;
}
return null;
}
}
class PaymobBillingDataWallet {
String? email;
String? firstName;
String? lastName;
String? phoneNumber;
String? apartment;
String? floor;
String? street;
String? building;
String? postalCode;
String? city;
String? state;
String? country;
String? shippingMethod;
PaymobBillingDataWallet({
this.email,
this.firstName,
this.lastName,
this.phoneNumber,
this.apartment,
this.floor,
this.street,
this.building,
this.postalCode,
this.city,
this.state,
this.country,
this.shippingMethod,
});
Map<String, dynamic> toJson() {
return {
"email": (box.read(BoxName.email)),
"first_name":
(box.read(BoxName.name).toString().split(' ')[0]).toString(),
"last_name":
(box.read(BoxName.name).toString().split(' ')[1]).toString() ??
'Intaleq',
"phone_number": (box.read(BoxName.phoneWallet)),
"apartment": apartment ?? "NA",
"floor": floor ?? "NA",
"building": building ?? "NA",
"street": street ?? "NA",
"postal_code": postalCode ?? "NA",
"city": city ?? "NA",
"state": state ?? "NA",
"country": country ?? "NA",
"shipping_method": shippingMethod ?? "NA",
};
}
}
class PaymobIFrameWallet extends StatefulWidget {
const PaymobIFrameWallet({
Key? key,
required this.redirectURL,
this.onPayment,
}) : super(key: key);
final String redirectURL;
final void Function(PaymobResponseWallet)? onPayment;
static Future<PaymobResponseWallet?> show({
required BuildContext context,
required String redirectURL,
void Function(PaymobResponseWallet)? onPayment,
}) =>
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return PaymobIFrameWallet(
onPayment: onPayment,
redirectURL: redirectURL,
);
},
),
);
@override
State<PaymobIFrameWallet> createState() => _PaymobIFrameState();
}
class _PaymobIFrameState extends State<PaymobIFrameWallet> {
WebViewController? controller;
@override
void initState() {
controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onNavigationRequest: (NavigationRequest request) {
if (request.url.contains('txn_response_code') &&
// request.url.contains('successfully') &&
request.url.contains('success') &&
request.url.contains('id')) {
final params = _getParamFromURL(request.url);
final response = PaymobResponseWallet.fromJson(params);
if (widget.onPayment != null) {
widget.onPayment!(response);
}
Navigator.pop(context, response);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
),
)
..loadRequest(Uri.parse(widget.redirectURL));
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: controller == null
? const Center(
child: CircularProgressIndicator.adaptive(),
)
: SafeArea(
child: WebViewWidget(
controller: controller!,
),
),
);
}
Map<String, dynamic> _getParamFromURL(String url) {
final uri = Uri.parse(url);
final queryParams = uri.queryParameters;
final data = <String, dynamic>{};
queryParams.forEach((key, value) {
if (key.contains('.')) {
final parts = key.split('.');
data.putIfAbsent(parts.first, () => <String, dynamic>{});
(data[parts.first] as Map<String, dynamic>)[parts.last] = value;
} else {
data[key] = value;
}
});
return data;
}
}

View File

@@ -0,0 +1,95 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/constant/links.dart';
import 'package:siro_rider/controller/functions/crud.dart';
import 'package:siro_rider/main.dart';
class CaptainProfileController extends GetxController {
bool isLoading = false;
TextEditingController vin = TextEditingController();
TextEditingController color = TextEditingController();
TextEditingController make = TextEditingController();
TextEditingController model = TextEditingController();
TextEditingController year = TextEditingController();
TextEditingController expirationDate = TextEditingController();
Future updateFields() async {
var payload = {
'driverID': box.read(BoxName.driverID),
};
if (vin.text.isNotEmpty) {
payload['vin'] = vin.text;
}
if (color.text.isNotEmpty) {
payload['color'] = color.text;
}
if (model.text.isNotEmpty) {
payload['model'] = model.text;
}
if (make.text.isNotEmpty) {
payload['make'] = make.text;
}
if (year.text.isNotEmpty) {
payload['year'] = year.text;
}
if (expirationDate.text.isNotEmpty) {
payload['expiration_date'] = expirationDate.text;
}
var res =
await CRUD().post(link: AppLink.updateRegisrationCar, payload: payload);
if (jsonDecode(res)['status'] == 'success') {
box.write(BoxName.vin, vin.text);
box.write(BoxName.color, color.text);
box.write(BoxName.model, model.text);
box.write(BoxName.make, make.text);
box.write(BoxName.year, year.text);
box.write(BoxName.expirationDate, expirationDate.text);
update();
Get.back();
}
}
Map captainProfileData = {};
Future getProfileData() async {
var res = await CRUD().get(
link: AppLink.getCaptainProfile,
payload: {'id': box.read(BoxName.driverID)});
if (res != 'failure') {
var d = jsonDecode(res);
captainProfileData = d['message'];
update();
box.write(BoxName.sexDriver, d['message']['gender']);
box.write(BoxName.dobDriver, d['message']['birthdate']);
box.write(BoxName.vin, d['message']['vin']);
box.write(BoxName.color, d['message']['color']);
box.write(BoxName.model, d['message']['model']);
box.write(BoxName.carPlate, d['message']['car_plate']);
box.write(BoxName.make, d['message']['make']);
box.write(BoxName.year, d['message']['year']);
box.write(BoxName.expirationDate, d['message']['expiration_date']);
// box.write(BoxName.acc, d['message']['accountBank']);
update();
}
}
@override
void onInit() {
// if (box.read(BoxName.dobDriver) == null) {
getProfileData();
// }
super.onInit();
}
}

View File

@@ -0,0 +1,117 @@
import 'dart:convert';
import 'package:siro_rider/constant/colors.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/constant/links.dart';
import 'package:siro_rider/controller/functions/crud.dart';
import 'package:siro_rider/main.dart';
class ProfileController extends GetxController {
bool isloading = false;
Map prfoileData = {};
TextEditingController txtController = TextEditingController();
List genders = ['Male', 'Female', 'Other'];
String gender = 'Male';
void setGender(String value) {
gender = value;
update();
}
String? selectedDegree;
void setDegree(String? degree) {
selectedDegree = degree;
update();
}
String? selectedCountry;
void setCountry(String? country) {
selectedCountry = country;
// box.write(BoxName.countryCode, country);
update();
}
updateColumn(Map<String, dynamic> payload) async {
isloading = true;
update();
await CRUD().post(link: AppLink.updateprofile, payload: payload);
await getProfile();
isloading = false;
update();
}
updatField(String columnName, TextInputType type) async {
Get.dialog(
CupertinoAlertDialog(
title: Text('${'Update'.tr} $columnName'),
content: Column(
children: [
const SizedBox(height: 16), // Add spacing between title and input
CupertinoTextField(
controller: txtController,
placeholder: 'type here'.tr,
keyboardType: type,
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
decoration: BoxDecoration(
border: Border.all(color: CupertinoColors.lightBackgroundGray),
borderRadius: BorderRadius.circular(8),
),
),
const SizedBox(height: 20),
CupertinoButton(
color: AppColor.blueColor,
onPressed: () async {
Get.back();
await updateColumn({
'id': box.read(BoxName.passengerID),
columnName: (txtController.text),
});
if (columnName == 'first_name') {
box.write(BoxName.name, (txtController.text));
}
txtController.clear();
},
child: Text('Update'.tr),
),
],
),
),
);
}
getProfile() async {
isloading = true;
update();
var res = await CRUD().get(link: AppLink.getprofile, payload: {
'id': box.read(BoxName.passengerID).toString(),
});
if (res.toString() == 'failure') {
// Get.snackbar('failure', 'message');
isloading = false;
update();
} else {
var jsonDecoded = jsonDecode(res);
prfoileData = jsonDecoded['data'];
box.write(BoxName.sosPhonePassenger, prfoileData['sosPhone'].toString());
box.write(BoxName.gender, prfoileData['gender'].toString());
box.write(BoxName.name,
'${prfoileData['first_name']} ${prfoileData['last_name']}');
isloading = false;
update();
}
}
@override
void onInit() {
getProfile();
super.onInit();
}
}

View File

@@ -0,0 +1,28 @@
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/main.dart';
import 'package:get/get.dart';
class SettingController extends GetxController {
bool isGoogleMapsEnabled = false;
void onChangMapApp() {
if (!isGoogleMapsEnabled) {
isGoogleMapsEnabled = true;
box.write(BoxName.googlaMapApp, true);
update();
} else {
isGoogleMapsEnabled = false;
box.write(BoxName.googlaMapApp, false);
update();
}
}
@override
void onInit() {
if (box.read(BoxName.googlaMapApp) != null) {
isGoogleMapsEnabled = box.read(BoxName.googlaMapApp);
}
update();
super.onInit();
}
}

View File

@@ -0,0 +1,101 @@
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/constant/links.dart';
import 'package:siro_rider/constant/style.dart';
import 'package:siro_rider/controller/functions/crud.dart';
import 'package:siro_rider/controller/home/map/ride_lifecycle_controller.dart';
import 'package:siro_rider/main.dart';
import 'package:siro_rider/views/home/map_page_passenger.dart';
import 'package:siro_rider/views/widgets/elevated_btn.dart';
import '../firebase/notification_service.dart';
import '../payment/payment_controller.dart';
// import '../home/captin/home_captain_controller.dart';
class RateController extends GetxController {
double selectedRateItemId = -1;
TextEditingController comment = TextEditingController();
String? rideId, passengerId, driverId, driverName, price;
late GlobalKey<FormState> formKey;
@override
void onInit() {
formKey = GlobalKey<FormState>();
passengerId = Get.arguments['passengerId'];
rideId = Get.arguments['rideId'];
driverId = Get.arguments['driverId'];
driverName = Get.arguments['driverName'];
price = Get.arguments['price'];
box.write(BoxName.tipPercentage, '0');
super.onInit();
}
void selectRateItem(double id) {
selectedRateItemId = id;
update();
}
addRateToDriver() async {
if (selectedRateItemId < 1) {
Get.defaultDialog(
title: 'You Should choose rate figure'.tr,
titleStyle: AppStyle.title,
middleText: '',
confirm: MyElevatedButton(title: 'Ok', onPressed: () => Get.back()));
} else if (Get.find<PaymentController>().isWalletChecked == true) {
double tip = 0;
tip = (Get.find<RideLifecycleController>().totalPassenger) *
(double.parse(box.read(BoxName.tipPercentage).toString()));
if (tip > 0) {
var res = await CRUD().post(link: AppLink.addTips, payload: {
'passengerID': box.read(BoxName.passengerID),
'driverID': Get.find<RideLifecycleController>().driverId.toString(),
'rideID': Get.find<RideLifecycleController>().rideId.toString(),
'tipAmount': tip.toString(),
});
await Get.find<PaymentController>()
.addPassengersWallet(((-1) * tip).toString());
var token1 = await Get.find<PaymentController>().generateTokenDriver(
box.read(BoxName.countryCode) == 'Egypt'
? tip.toStringAsFixed(0)
: (tip * 100).toString());
await CRUD().postWallet(link: AppLink.addDriversWalletPoints, payload: {
'driverID': Get.find<RideLifecycleController>().driverId.toString(),
'paymentID': '${Get.find<RideLifecycleController>().rideId}tip',
'amount': box.read(BoxName.countryCode) == 'Egypt'
? tip.toStringAsFixed(0)
: (tip * 100).toString(),
'paymentMethod': 'visa-tip',
'token': token1,
});
if (res != 'failure') {
await NotificationService.sendNotification(
category: 'You Have Tips',
target: Get.find<RideLifecycleController>().driverToken.toString(),
title: 'You Have Tips'.tr,
body:
'${'${tip.toString()}\$${' tips\nTotal is'.tr}'} ${tip + (Get.find<RideLifecycleController>().totalPassenger)}',
isTopic: false, // Important: this is a token
tone: 'ding',
driverList: [],
);
}
}
}
await CRUD().post(
link: "${AppLink.server}/ride/rate/addRateToDriver.php",
payload: {
'passenger_id': box.read(BoxName.passengerID).toString(),
'driver_id': driverId.toString(),
'ride_id': rideId.toString(),
'rating': selectedRateItemId.toString(),
'comment': comment.text,
},
);
Get.find<RideLifecycleController>().restCounter();
Get.offAll(const MapPagePassenger());
}
}

View File

@@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
import 'package:siro_rider/constant/style.dart';
import '../../constant/colors.dart';
ThemeData lightThemeEnglish = ThemeData(
brightness: Brightness.light,
fontFamily: "SFPro",
textTheme: TextTheme(
displaySmall: AppStyle.title,
displayLarge: AppStyle.headTitle,
displayMedium: AppStyle.headTitle2,
bodyLarge: AppStyle.title,
bodyMedium: AppStyle.subtitle,
),
primarySwatch: Colors.blue,
dialogTheme: DialogThemeData(
backgroundColor: AppColor.secondaryColor,
contentTextStyle: AppStyle.title,
titleTextStyle: AppStyle.headTitle2,
),
appBarTheme: AppBarTheme(
elevation: 0,
color: AppColor.secondaryColor,
centerTitle: true,
iconTheme: const IconThemeData(
color: AppColor.primaryColor,
),
toolbarTextStyle: TextTheme(
titleSmall: AppStyle.subtitle,
headlineSmall: AppStyle.title,
titleLarge: AppStyle.headTitle2,
).bodyMedium,
titleTextStyle: TextTheme(
titleSmall: AppStyle.subtitle,
headlineSmall: AppStyle.title,
titleLarge: AppStyle.headTitle2,
).titleLarge,
),
);
ThemeData darkThemeEnglish = ThemeData(
brightness: Brightness.dark,
fontFamily: "SFPro",
primaryColor: AppColor.primaryColor,
scaffoldBackgroundColor: const Color(0xFF121212),
cardColor: const Color(0xFF1E1E1E),
textTheme: TextTheme(
displaySmall: AppStyle.title.copyWith(color: Colors.white),
displayLarge: AppStyle.headTitle.copyWith(color: Colors.white),
displayMedium: AppStyle.headTitle2.copyWith(color: Colors.white),
bodyLarge: AppStyle.title.copyWith(color: Colors.white70),
bodyMedium: AppStyle.subtitle.copyWith(color: Colors.white60),
),
primarySwatch: Colors.green,
dialogTheme: const DialogThemeData(
backgroundColor: Color(0xFF1E1E1E),
contentTextStyle: TextStyle(color: Colors.white),
titleTextStyle: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
appBarTheme: const AppBarTheme(
elevation: 0,
color: Color(0xFF121212),
centerTitle: true,
iconTheme: IconThemeData(
color: Colors.white,
),
titleTextStyle: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
);
ThemeData lightThemeArabic = ThemeData(
brightness: Brightness.light,
fontFamily: 'SFArabic',
textTheme: TextTheme(
displaySmall: AppStyle.title,
displayLarge: AppStyle.headTitle,
displayMedium: AppStyle.headTitle2,
bodyLarge: AppStyle.title,
bodyMedium: AppStyle.subtitle,
),
primarySwatch: Colors.blue,
dialogTheme: DialogThemeData(
backgroundColor: AppColor.secondaryColor,
contentTextStyle: AppStyle.title,
titleTextStyle: AppStyle.headTitle2,
),
appBarTheme: AppBarTheme(
elevation: 0,
color: AppColor.secondaryColor,
centerTitle: true,
iconTheme: const IconThemeData(
color: AppColor.primaryColor,
),
toolbarTextStyle: TextTheme(
titleSmall: AppStyle.subtitle,
headlineSmall: AppStyle.title,
titleLarge: AppStyle.headTitle2,
).bodyMedium,
titleTextStyle: TextTheme(
titleSmall: AppStyle.subtitle,
headlineSmall: AppStyle.title,
titleLarge: AppStyle.headTitle2,
).titleLarge,
),
);
ThemeData darkThemeArabic = ThemeData(
brightness: Brightness.dark,
fontFamily: 'SFArabic',
primaryColor: AppColor.primaryColor,
scaffoldBackgroundColor: const Color(0xFF121212),
cardColor: const Color(0xFF1E1E1E),
textTheme: TextTheme(
displaySmall: AppStyle.title.copyWith(color: Colors.white),
displayLarge: AppStyle.headTitle.copyWith(color: Colors.white),
displayMedium: AppStyle.headTitle2.copyWith(color: Colors.white),
bodyLarge: AppStyle.title.copyWith(color: Colors.white70),
bodyMedium: AppStyle.subtitle.copyWith(color: Colors.white60),
),
primarySwatch: Colors.green,
dialogTheme: const DialogThemeData(
backgroundColor: Color(0xFF1E1E1E),
contentTextStyle: TextStyle(color: Colors.white),
titleTextStyle: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
appBarTheme: const AppBarTheme(
elevation: 0,
color: Color(0xFF121212),
centerTitle: true,
iconTheme: IconThemeData(
color: Colors.white,
),
titleTextStyle: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
);

View File

@@ -0,0 +1,722 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'package:get/get.dart' hide Response;
import 'package:permission_handler/permission_handler.dart';
import 'package:just_audio/just_audio.dart';
import '../../constant/box_name.dart';
import '../../constant/links.dart';
import '../../main.dart';
import '../../print.dart';
import '../../services/signaling_service.dart';
import '../../views/widgets/voice_call_bottom_sheet.dart';
import 'functions/crud.dart';
// EN: Enum representing the different states of a voice call.
// AR: تعداد يمثل الحالات المختلفة للمكالمة الصوتية.
enum VoiceCallState { idle, dialing, ringing, connecting, active, ended }
class VoiceCallController extends GetxController with WidgetsBindingObserver {
// EN: Instance of the signaling service to manage WebSocket communication.
// AR: مثيل لخدمة الإشارات لإدارة الاتصال عبر الـ WebSocket.
final SignalingService _signaling = SignalingService();
// --- Observable Variables (GetX) / المتغيرات التفاعلية ---
// EN: Current state of the call.
// AR: الحالة الحالية للمكالمة.
var state = VoiceCallState.idle.obs;
// EN: Unique identifier for the WebRTC session.
// AR: المعرف الفريد لجلسة الاتصال.
var sessionId = "".obs;
// EN: ID of the current active ride.
// AR: معرف الرحلة النشطة الحالية.
var rideId = "".obs;
// EN: Name of the other party (Driver/Passenger).
// AR: اسم الطرف الآخر في المكالمة (سائق/راكب).
var remoteName = "User".obs;
// EN: Microphone mute status.
// AR: حالة كتم الميكروفون.
var isMuted = false.obs;
// EN: Speakerphone status.
// AR: حالة مكبر الصوت الخارجي.
var isSpeakerOn = false.obs;
// EN: Timer countdown variable, starts from 60 seconds.
// AR: متغير العد التنازلي للمؤقت، يبدأ من 60 ثانية.
var elapsedSeconds = 60.obs;
// --- Core State Variables / متغيرات الحالة الأساسية ---
// EN: Flag to determine if the current user initiated the call.
// AR: مؤشر لتحديد ما إذا كان المستخدم الحالي هو من بدأ المكالمة.
bool isCaller = false;
// EN: ID of the current user.
// AR: معرف المستخدم الحالي.
String currentUserId = "";
// --- WebRTC Internal Variables / متغيرات WebRTC الداخلية ---
// EN: The main connection object between peers.
// AR: كائن الاتصال الرئيسي بين الطرفين.
rtc.RTCPeerConnection? _peerConnection;
// EN: The local audio stream captured from the microphone.
// AR: دفق الصوت المحلي الملتقط من الميكروفون.
rtc.MediaStream? _localStream;
// EN: Timer to enforce the 60-second call limit.
// AR: مؤقت لفرض حد الـ 60 ثانية للمكالمة.
Timer? _countdownTimer;
// EN: Timer to hang up if the call is not answered within 30 seconds.
// AR: مؤقت لإنهاء المكالمة إذا لم يتم الرد خلال 30 ثانية.
Timer? _ringingTimeoutTimer;
// EN: Flag to indicate if the peer connection is currently attempting ICE reconnection.
// AR: مؤشر يوضح ما إذا كان الاتصال يحاول إعادة بناء مسارات الشبكة حالياً.
bool _isReconnecting = false;
Timer? _reconnectTimer;
List<dynamic> _dynamicIceServers = [];
AudioPlayer? _ringtonePlayer;
void _startRingtone() async {
try {
_ringtonePlayer ??= AudioPlayer();
await _ringtonePlayer!.setAsset('assets/start.wav');
await _ringtonePlayer!.setLoopMode(LoopMode.one);
_ringtonePlayer!.play();
} catch (e) {
Log.print("Error playing ringtone: $e");
}
}
void _stopRingtone() {
try {
_ringtonePlayer?.stop();
} catch (e) {
Log.print("Error stopping ringtone: $e");
}
}
@override
void onInit() {
super.onInit();
// EN: Add lifecycle observer.
// AR: إضافة مراقب لدورة حياة التطبيق.
WidgetsBinding.instance.addObserver(this);
// EN: Initialize WebSocket signaling listeners.
// AR: تهيئة مستمعي إشارات الـ WebSocket.
_initSignalingCallbacks();
}
// EN: Lifecycle hook: handle app switching background/foreground.
// AR: معالجة انتقال التطبيق إلى الخلفية أو العودة للواجهة.
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
Log.print("VoiceCall: didChangeAppLifecycleState -> $state");
if (state == AppLifecycleState.paused) {
Log.print(
"WARNING: App is in background. Microphone access might be suspended by the OS.",
);
} else if (state == AppLifecycleState.resumed) {
Log.print("App resumed. Verifying WebRTC connection health.");
if (this.state.value == VoiceCallState.active) {
_ensureMicrophoneActive();
_attemptIceRestart();
}
}
}
// EN: Registers all event listeners for the signaling server.
// AR: تسجيل جميع مستمعي الأحداث لخادم الإشارات.
void _initSignalingCallbacks() {
// EN: Triggered when successfully connected to the signaling server.
// AR: يُستدعى عند الاتصال بنجاح بخادم الإشارات.
_signaling.onConnected = (iceServers) {
Log.print("WebRTC Signaling Connected & Authenticated");
_dynamicIceServers = iceServers;
};
// EN: Triggered when the WebSocket connection drops.
// AR: يُستدعى عند انقطاع اتصال الـ WebSocket.
_signaling.onDisconnected = (reason) {
Log.print("WebRTC Signaling Disconnected: $reason");
if (state.value != VoiceCallState.idle) {
_endCallInternal("signaling_disconnected");
}
};
// EN: Triggered when the remote user joins the room.
// AR: يُستدعى عند انضمام الطرف الآخر إلى غرفة الاتصال.
_signaling.onParticipantJoined = () async {
Log.print("Remote participant joined signaling session");
// EN: If we are the caller, initiate the WebRTC handshake by creating an Offer.
// AR: إذا كنا نحن المتصل، نبدأ مصافحة WebRTC بإنشاء عرض (Offer).
if (isCaller && state.value == VoiceCallState.dialing) {
state.value = VoiceCallState.connecting;
await _createOffer();
}
};
// EN: Triggered when an SDP Offer is received from the remote peer.
// AR: يُستدعى عند استلام عرض اتصال (Offer) من الطرف الآخر.
_signaling.onOffer = (sdpMap) async {
Log.print("Received WebRTC SDP Offer");
if (!isCaller) {
state.value = VoiceCallState.connecting;
await _initializePeerConnection();
// EN: Set the remote peer's settings.
// AR: تعيين إعدادات الطرف الآخر.
final description = rtc.RTCSessionDescription(
sdpMap['sdp'],
sdpMap['type'],
);
await _peerConnection!.setRemoteDescription(description);
// EN: Respond with an Answer.
// AR: الرد بإجابة (Answer).
await _createAnswer();
}
};
// EN: Triggered when an SDP Answer is received.
// AR: يُستدعى عند استلام إجابة (Answer) من الطرف الآخر.
_signaling.onAnswer = (sdpMap) async {
Log.print("Received WebRTC SDP Answer");
if (isCaller && _peerConnection != null) {
final description = rtc.RTCSessionDescription(
sdpMap['sdp'],
sdpMap['type'],
);
await _peerConnection!.setRemoteDescription(description);
}
};
// EN: Triggered when ICE candidates (Network routing info) are exchanged.
// AR: يُستدعى عند تبادل مسارات الشبكة (ICE Candidates) لتأسيس الاتصال.
_signaling.onIceCandidate = (candidateMap) async {
Log.print("Received Remote ICE Candidate");
if (_peerConnection != null) {
final candidate = rtc.RTCIceCandidate(
candidateMap['candidate'],
candidateMap['sdpMid'],
candidateMap['sdpMLineIndex'],
);
await _peerConnection!.addCandidate(candidate);
}
};
// EN: Triggered when a hangup event is received from the server.
// AR: يُستدعى عند استلام حدث إنهاء المكالمة من السيرفر.
_signaling.onCallEnded = (reason) {
Log.print("WebRTC Call Ended: $reason");
_endCallInternal(reason);
};
}
// --- CALL LIFECYCLE / دورة حياة المكالمة ---
// EN: Initiates an outgoing call.
// AR: يبدأ مكالمة صادرة.
Future<void> startCall({
required String rideIdVal,
required String driverId,
required String passengerId,
required String remoteNameVal,
}) async {
if (state.value != VoiceCallState.idle) return;
// EN: Setup call variables.
// AR: إعداد متغيرات المكالمة.
state.value = VoiceCallState.dialing;
isCaller = true;
currentUserId = passengerId;
rideId.value = rideIdVal;
remoteName.value = remoteNameVal;
isMuted.value = false;
isSpeakerOn.value = false;
elapsedSeconds.value = 60;
_isReconnecting = false;
_showCallBottomSheet();
HapticFeedback.vibrate();
try {
// 1. EN: Request Microphone Permission / AR: طلب صلاحية الميكروفون
final permissionStatus = await Permission.microphone.request();
if (!permissionStatus.isGranted) {
_endCallInternal("permission_denied");
Get.snackbar(
"Error",
"Microphone permission is required for voice calls".tr,
);
return;
}
// 2. EN: Call PHP Backend to create Node.js session & notify Driver via FCM.
// AR: استدعاء واجهة PHP لإنشاء الجلسة على Node.js وإشعار السائق عبر FCM.
final response = await CRUD().post(
link: "${AppLink.server}/ride/call/passenger/create_call_session.php",
payload: {'ride_id': rideIdVal},
);
if (response == null ||
response == 'failure' ||
response['status'] != 'success') {
_endCallInternal("session_creation_failed");
Get.snackbar(
"Error",
"Failed to initiate call session. Please try again.".tr,
);
return;
}
final data = response['data'];
sessionId.value = data['session_id'];
// 3. EN: Connect to WebRTC signaling server / AR: الاتصال بخادم الإشارات
await _signaling.connect(sessionId.value, currentUserId);
// 4. EN: Initialize Local WebRTC Audio Stream / AR: تهيئة دفق الصوت المحلي
await _initializeLocalStream();
// 5. EN: Start Ringing Timeout Timer (30s max wait for driver to answer).
// AR: بدء مؤقت الرنين (أقصى انتظار 30 ثانية لرد السائق).
_ringingTimeoutTimer = Timer(const Duration(seconds: 30), () {
if (state.value == VoiceCallState.dialing) {
_signaling.send("hangup", {"reason": "no_answer"});
_endCallInternal("no_answer");
}
});
} catch (e) {
Log.print("Error starting WebRTC call: $e");
_endCallInternal("error");
}
}
// EN: Handles incoming call requests via FCM/Socket.
// AR: معالجة طلبات المكالمات الواردة.
Future<void> receiveCall({
required String sessionIdVal,
required String remoteNameVal,
required String rideIdVal,
}) async {
// EN: If already in a call, send busy signal.
// AR: إذا كان في مكالمة بالفعل، إرسال إشارة مشغول.
if (state.value != VoiceCallState.idle) {
_signaling.send("hangup", {"reason": "busy"});
return;
}
state.value = VoiceCallState.ringing;
isCaller = false;
currentUserId = box.read(BoxName.passengerID).toString();
sessionId.value = sessionIdVal;
rideId.value = rideIdVal;
remoteName.value = remoteNameVal;
isMuted.value = false;
isSpeakerOn.value = false;
elapsedSeconds.value = 60;
_isReconnecting = false;
_showCallBottomSheet();
_startRingtone();
HapticFeedback.vibrate();
// EN: Max 30s ringing timeout for receiver before auto-decline.
// AR: أقصى مدة للرنين 30 ثانية قبل الرفض التلقائي.
_ringingTimeoutTimer = Timer(const Duration(seconds: 30), () {
if (state.value == VoiceCallState.ringing) {
declineCall();
}
});
}
// EN: Accepts the incoming call.
// AR: قبول المكالمة الواردة.
Future<void> acceptCall() async {
if (state.value != VoiceCallState.ringing) return;
_ringingTimeoutTimer?.cancel();
_stopRingtone();
state.value = VoiceCallState.connecting;
try {
// EN: Check Mic permissions / AR: التحقق من صلاحيات الميكروفون
final permissionStatus = await Permission.microphone.request();
if (!permissionStatus.isGranted) {
declineCall();
Get.snackbar(
"Error",
"Microphone permission is required for voice calls".tr,
);
return;
}
await _signaling.connect(sessionId.value, currentUserId);
await _initializeLocalStream();
// EN: Notify caller we accepted / AR: إشعار المتصل بأننا قبلنا المكالمة
_signaling.send("join", {});
} catch (e) {
Log.print("Error accepting call: $e");
declineCall();
}
}
// EN: Declines an incoming call.
// AR: رفض المكالمة الواردة.
void declineCall() {
_ringingTimeoutTimer?.cancel();
_stopRingtone();
_signaling.send("hangup", {"reason": "declined"});
_endCallInternal("declined");
}
// EN: Ends an active or dialing call.
// AR: إنهاء المكالمة النشطة أو الجاري الاتصال بها.
void hangup() {
_signaling.send("hangup", {"reason": "normal"});
_endCallInternal("hangup");
}
// --- WEBRTC CORE HELPERS / دوال WebRTC الأساسية ---
// EN: Captures the audio from the microphone with optimization constraints.
// AR: التقاط الصوت من الميكروفون مع قيود تحسين الجودة (إلغاء الصدى والضوضاء).
Future<void> _initializeLocalStream() async {
final Map<String, dynamic> mediaConstraints = {
'audio': {
'echoCancellation': true,
'noiseSuppression': true,
'autoGainControl': true,
},
'video': false, // EN: Audio only / AR: صوت فقط
};
_localStream = await rtc.navigator.mediaDevices.getUserMedia(
mediaConstraints,
);
rtc.Helper.setSpeakerphoneOn(isSpeakerOn.value);
}
// EN: Verifies local microphone stream health on app resume and recreates/replaces track if suspended.
// AR: التحقق من سلامة مسار الميكروفون المحلي عند استئناف التطبيق وإعادة إنشائه إذا تم تعليقه.
Future<void> _ensureMicrophoneActive() async {
if (_localStream == null || _peerConnection == null) return;
bool needsRecreation = false;
if (_localStream!.active == false) {
needsRecreation = true;
} else {
for (var track in _localStream!.getAudioTracks()) {
if (!track.enabled && !isMuted.value) {
needsRecreation = true;
break;
}
}
}
if (needsRecreation) {
Log.print(
"Local audio track ended or disabled. Recreating local stream...",
);
try {
_localStream?.getTracks().forEach((track) => track.stop());
_localStream?.dispose();
_localStream = null;
await _initializeLocalStream();
final senders = await _peerConnection!.getSenders();
for (var sender in senders) {
final track = sender.track;
if (track != null && track.kind == 'audio') {
final newTracks = _localStream?.getAudioTracks();
if (newTracks != null && newTracks.isNotEmpty) {
await sender.replaceTrack(newTracks.first);
Log.print(
"Replaced suspended/ended audio track with a new active one.",
);
}
break;
}
}
} catch (e) {
Log.print("Error recreating local stream on resume: $e");
}
} else {
_localStream!.getAudioTracks().forEach((track) {
track.enabled = !isMuted.value;
});
}
}
// EN: Creates the peer connection object and sets up ICE servers (STUN/TURN).
// AR: إنشاء كائن الاتصال المباشر وإعداد خوادم STUN/TURN لاختراق الجدران النارية.
Future<void> _initializePeerConnection() async {
if (_peerConnection != null) return;
final List<Map<String, dynamic>> iceServers = [];
if (_dynamicIceServers.isNotEmpty) {
for (var server in _dynamicIceServers) {
if (server is Map) {
iceServers.add({
"urls": server["urls"] ?? server["url"],
if (server["username"] != null) "username": server["username"],
if (server["credential"] != null)
"credential": server["credential"],
});
}
}
} else {
// EN: Fallback STUN servers / AR: خوادم STUN الاحتياطية
iceServers.addAll([
{"urls": "stun:stun.l.google.com:19302"},
{"urls": "stun:stun1.l.google.com:19302"},
]);
}
final Map<String, dynamic> configuration = {"iceServers": iceServers};
_peerConnection = await rtc.createPeerConnection(configuration);
// EN: Gather local network routing info and send to remote peer.
// AR: جمع بيانات مسارات الشبكة المحلية وإرسالها للطرف الآخر.
_peerConnection!.onIceCandidate = (candidate) {
if (candidate.candidate != null) {
_signaling.send("ice_candidate", {
"candidate": {
"candidate": candidate.candidate,
"sdpMid": candidate.sdpMid,
"sdpMLineIndex": candidate.sdpMLineIndex,
},
});
}
};
// EN: Monitor connection status changes and handle disconnections.
// AR: مراقبة تغيرات حالة الاتصال ومعالجة انقطاع الشبكة.
_peerConnection!.onConnectionState = (connState) {
Log.print("RTCPeerConnectionState: $connState");
if (connState ==
rtc.RTCPeerConnectionState.RTCPeerConnectionStateConnected) {
_onCallConnected();
} else if (connState ==
rtc.RTCPeerConnectionState.RTCPeerConnectionStateFailed ||
connState ==
rtc.RTCPeerConnectionState.RTCPeerConnectionStateDisconnected) {
_handleIceConnectionFailure();
}
};
// EN: Add local audio stream to the connection to send it to the other peer.
// AR: إضافة دفق الصوت المحلي للاتصال لإرساله للطرف الآخر.
if (_localStream != null) {
_localStream!.getTracks().forEach((track) {
_peerConnection!.addTrack(track, _localStream!);
});
}
}
// EN: Attempts an ICE restart to reconnect the WebRTC session when disconnections occur.
// AR: محاولة إعادة تأسيس الاتصال (ICE Restart) في حالة انقطاع الشبكة.
void _handleIceConnectionFailure() {
if (_isReconnecting) return;
_isReconnecting = true;
Log.print(
"ICE connection dropped. Attempting ICE Restart reconnection for 5s...",
);
if (isCaller) {
_attemptIceRestart();
}
_reconnectTimer?.cancel();
_reconnectTimer = Timer(const Duration(seconds: 5), () {
if (state.value == VoiceCallState.active &&
_peerConnection?.connectionState !=
rtc.RTCPeerConnectionState.RTCPeerConnectionStateConnected) {
Log.print("ICE reconnection timed out. Hanging up.");
_endCallInternal("connection_lost");
} else {
_isReconnecting = false;
Log.print("ICE Reconnection succeeded!");
}
});
}
// EN: Initiates ICE Restart SDP exchange.
// AR: بدء تبادل حزم SDP لإعادة بناء مسارات الاتصال.
Future<void> _attemptIceRestart() async {
if (_peerConnection == null || !isCaller) return;
try {
Log.print("Caller initiating WebRTC ICE Restart...");
final constraints = {
'mandatory': {
'OfferToReceiveAudio': true,
'OfferToReceiveVideo': false,
},
'optional': [
{'IceRestart': true},
],
};
final offer = await _peerConnection!.createOffer(constraints);
await _peerConnection!.setLocalDescription(offer);
_signaling.send("offer", {
"sdp": {"sdp": offer.sdp, "type": offer.type},
});
} catch (e) {
Log.print("Error initiating WebRTC ICE Restart: $e");
}
}
// EN: Generates an SDP Offer to initialize the connection.
// AR: إنشاء عرض (Offer) لبدء الاتصال وتحديد قدرات الجهاز.
Future<void> _createOffer() async {
await _initializePeerConnection();
final constraints = {
'mandatory': {'OfferToReceiveAudio': true, 'OfferToReceiveVideo': false},
'optional': [],
};
final offer = await _peerConnection!.createOffer(constraints);
await _peerConnection!.setLocalDescription(offer);
_signaling.send("offer", {
"sdp": {"sdp": offer.sdp, "type": offer.type},
});
}
// EN: Generates an SDP Answer in response to an Offer.
// AR: الرد بإنشاء إجابة (Answer) بناءً على العرض المستلم.
Future<void> _createAnswer() async {
final constraints = {
'mandatory': {'OfferToReceiveAudio': true, 'OfferToReceiveVideo': false},
'optional': [],
};
final answer = await _peerConnection!.createAnswer(constraints);
await _peerConnection!.setLocalDescription(answer);
_signaling.send("answer", {
"sdp": {"sdp": answer.sdp, "type": answer.type},
});
}
// EN: Triggered when connection is fully established. Starts the 60s timer.
// AR: يُستدعى عند تأسيس الاتصال بنجاح، ويقوم ببدء مؤقت الـ 60 ثانية.
void _onCallConnected() {
_ringingTimeoutTimer?.cancel();
_reconnectTimer?.cancel();
_isReconnecting = false;
if (state.value != VoiceCallState.active) {
state.value = VoiceCallState.active;
HapticFeedback.vibrate();
// EN: Start 120s countdown timer / AR: بدء العد التنازلي لمدة 120 ثانية
_countdownTimer?.cancel();
elapsedSeconds.value = 120;
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (elapsedSeconds.value > 1) {
elapsedSeconds.value--;
} else {
elapsedSeconds.value = 0;
_countdownTimer?.cancel();
// EN: Force hangup when timer reaches 0 / AR: إغلاق إجباري عند وصول المؤقت لصفر
hangup();
}
});
}
}
// EN: Internal cleanup function. Closes all connections and streams.
// AR: دالة التنظيف الداخلية. تقوم بإغلاق جميع الاتصالات وتفريغ الذاكرة.
void _endCallInternal(String reason) {
_countdownTimer?.cancel();
_ringingTimeoutTimer?.cancel();
_reconnectTimer?.cancel();
_stopRingtone();
state.value = VoiceCallState.ended;
// EN: Close WebRTC connection / AR: إغلاق اتصال WebRTC
_peerConnection?.close();
_peerConnection = null;
// EN: Stop mic capture / AR: إيقاف التقاط الميكروفون
_localStream?.getTracks().forEach((track) => track.stop());
_localStream?.dispose();
_localStream = null;
// EN: Disconnect WebSockets / AR: إغلاق اتصال الـ WebSockets
_signaling.disconnect();
// EN: Close UI BottomSheet after delay / AR: إغلاق واجهة المكالمة بعد فترة زمنية قصيرة
Future.delayed(const Duration(milliseconds: 1500), () {
if (state.value == VoiceCallState.ended) {
state.value = VoiceCallState.idle;
Get.back();
}
});
}
// --- ACTIONS (UI Controls) / إجراءات الواجهة ---
// EN: Toggles microphone mute state.
// AR: تبديل حالة كتم الميكروفون.
void toggleMute() {
isMuted.value = !isMuted.value;
_localStream?.getAudioTracks().forEach((track) {
track.enabled = !isMuted.value;
});
}
// EN: Toggles loudspeaker mode.
// AR: تبديل حالة مكبر الصوت الخارجي.
void toggleSpeaker() {
isSpeakerOn.value = !isSpeakerOn.value;
rtc.Helper.setSpeakerphoneOn(isSpeakerOn.value);
}
// EN: Displays the call UI overlay.
// AR: إظهار نافذة المكالمة السفلية.
void _showCallBottomSheet() {
Get.bottomSheet(
const VoiceCallBottomSheet(),
isScrollControlled: true,
enableDrag: false,
isDismissible: false,
);
}
// EN: Lifecycle hook: clean up resources when controller is destroyed.
// AR: دورة الحياة: تفريغ الذاكرة وإغلاق الموارد عند تدمير المتحكم.
@override
void onClose() {
WidgetsBinding.instance.removeObserver(this);
_countdownTimer?.cancel();
_ringingTimeoutTimer?.cancel();
_reconnectTimer?.cancel();
_stopRingtone();
_ringtonePlayer?.dispose();
_peerConnection?.close();
_localStream?.dispose();
_signaling.disconnect();
super.onClose();
}
}