diff --git a/android/app/build.gradle b/android/app/build.gradle index 38365ea..ccbf322 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -46,8 +46,8 @@ android { // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = 29 targetSdk = 36 - versionCode = 13 - versionName = '1.0.13' + versionCode = 14 + versionName = '1.0.14' multiDexEnabled =true } diff --git a/assets/images/cashMTN.png b/assets/images/cashMTN.png new file mode 100644 index 0000000..bae958b Binary files /dev/null and b/assets/images/cashMTN.png differ diff --git a/assets/images/shamCash.png b/assets/images/shamCash.png new file mode 100644 index 0000000..ab13ac0 Binary files /dev/null and b/assets/images/shamCash.png differ diff --git a/assets/images/syriatel.jpeg b/assets/images/syriatel.jpeg new file mode 100644 index 0000000..331117b Binary files /dev/null and b/assets/images/syriatel.jpeg differ diff --git a/lib/constant/box_name.dart b/lib/constant/box_name.dart index c79b822..ffddc57 100755 --- a/lib/constant/box_name.dart +++ b/lib/constant/box_name.dart @@ -11,6 +11,7 @@ class BoxName { "rideArgumentsFromBackground"; static const String FCM_PRIVATE_KEY = "FCM_PRIVATE_KEY"; static const String hmac = "hmac"; + static const String walletType = "walletType"; static const String fingerPrint = "fingerPrint"; static const String updateInterval = "updateInterval"; static const String payMobApikey = "payMobApikey"; diff --git a/lib/constant/links.dart b/lib/constant/links.dart index 3c83edc..ca5882f 100755 --- a/lib/constant/links.dart +++ b/lib/constant/links.dart @@ -47,6 +47,8 @@ class AppLink { static String getDriverToken = "$ride/firebase/getDriverToken.php"; static String addTokens = "$ride/firebase/add.php"; static String addTokensDriver = "$ride/firebase/addDriver.php"; + static String addTokensDriverWallet = + "$seferPaymentServer/ride/firebase/addDriver.php"; //=======================Wallet=================== static String wallet = '$seferPaymentServer/ride/passengerWallet'; @@ -57,6 +59,10 @@ class AppLink { "$seferPaymentServer/ride/mtn/driver/confirm_payment.php"; static String payWithMTNStart = "$seferPaymentServer/ride/mtn/driver/mtn_start.php"; + static String payWithSyriatelConfirm = + "$seferPaymentServer/ride/syriatel/driver/confirm_payment.php"; + static String payWithSyriatelStart = + "$seferPaymentServer/ride/syriatel/driver/start_payment.php"; static String payWithEcashDriver = "$seferPaymentServer/ride/ecash/driver/payWithEcash.php"; static String payWithEcashPassenger = diff --git a/lib/controller/auth/captin/login_captin_controller.dart b/lib/controller/auth/captin/login_captin_controller.dart index 0a5d852..7297c38 100755 --- a/lib/controller/auth/captin/login_captin_controller.dart +++ b/lib/controller/auth/captin/login_captin_controller.dart @@ -21,6 +21,7 @@ import 'package:location/location.dart'; import '../../../constant/api_key.dart'; import '../../../constant/info.dart'; +import '../../../print.dart'; import '../../../views/auth/captin/otp_page.dart'; import '../../../views/auth/captin/otp_token_page.dart'; import '../../../views/auth/syria/pending_driver_page.dart'; @@ -98,11 +99,10 @@ class LoginDriverController extends GetxController { } isPhoneVerified() async { - var res = await CRUD().post(link: AppLink.isPhoneVerified, payload: { - 'phone_number': box.read( - BoxName.phoneDriver, - ) - }); + var res = await CRUD().post( + link: AppLink.isPhoneVerified, + payload: {'phone_number': box.read(BoxName.phoneDriver)}); + if (res != 'failure') { // Get.offAll(() => SyrianCardAI()); Get.offAll(() => RegistrationView()); @@ -163,8 +163,8 @@ class LoginDriverController extends GetxController { getJWT() async { dev = Platform.isAndroid ? 'android' : 'ios'; - // Log.print( - // 'box.read(BoxName.firstTimeLoadKey): ${box.read(BoxName.firstTimeLoadKey)}'); + Log.print( + 'box.read(BoxName.firstTimeLoadKey): ${box.read(BoxName.firstTimeLoadKey)}'); if (box.read(BoxName.firstTimeLoadKey).toString() != 'false') { var payload = { 'id': box.read(BoxName.driverID) ?? AK.newId, @@ -185,13 +185,6 @@ class LoginDriverController extends GetxController { final jwt = decodedResponse1['jwt']; box.write(BoxName.jwt, c(jwt)); - // await box.write(BoxName.hmac, decodedResponse1['hmac']); - - // await AppInitializer().getAIKey(Driver.payMobApikey); - // await AppInitializer().getAIKey(Driver.FCM_PRIVATE_KEY); - - // await AppInitializer().getAIKey(Driver.initializationVector); - // await AppInitializer().getAIKey(Driver.keyOfApp); // ✅ بعد التأكد أن كل المفاتيح موجودة await EncryptionHelper.initialize(); @@ -206,7 +199,7 @@ class LoginDriverController extends GetxController { 'password': box.read(BoxName.emailDriver), 'aud': '${AK.allowed}$dev', }; - // print(payload); + print(payload); var response1 = await http.post( Uri.parse(AppLink.loginJwtDriver), body: payload, @@ -332,32 +325,30 @@ class LoginDriverController extends GetxController { key: BoxName.fingerPrint, value: fingerPrint.toString()); // print(jsonDecode(token)['data'][0]['token'].toString()); // print(box.read(BoxName.tokenDriver).toString()); - if (email == '962798583052@intaleqapp.com') { - } else { - if (token != 'failure') { - if ((jsonDecode(token)['data'][0]['token'].toString()) != - box.read(BoxName.tokenDriver).toString()) { - await Get.defaultDialog( - barrierDismissible: false, - title: 'Device Change Detected'.tr, - middleText: 'Please verify your identity'.tr, - textConfirm: 'Verify'.tr, - confirmTextColor: Colors.white, - onConfirm: () { - // Get.back(); - // انتقل لصفحة OTP الجديدة - Get.to( - () => OtpVerificationPage( - phone: d['phone'].toString(), - deviceToken: fingerPrint.toString(), - token: token.toString(), - ptoken: - jsonDecode(token)['data'][0]['token'].toString(), - ), - ); - }, - ); - } + + if (token != 'failure') { + if ((jsonDecode(token)['data'][0]['token'].toString()) != + box.read(BoxName.tokenDriver).toString()) { + await Get.defaultDialog( + barrierDismissible: false, + title: 'Device Change Detected'.tr, + middleText: 'Please verify your identity'.tr, + textConfirm: 'Verify'.tr, + confirmTextColor: Colors.white, + onConfirm: () { + // Get.back(); + // انتقل لصفحة OTP الجديدة + Get.to( + () => OtpVerificationPage( + phone: d['phone'].toString(), + deviceToken: fingerPrint.toString(), + token: token.toString(), + ptoken: + jsonDecode(token)['data'][0]['token'].toString(), + ), + ); + }, + ); } } diff --git a/lib/controller/auth/captin/opt_token_controller.dart b/lib/controller/auth/captin/opt_token_controller.dart index c0b10ff..5250026 100644 --- a/lib/controller/auth/captin/opt_token_controller.dart +++ b/lib/controller/auth/captin/opt_token_controller.dart @@ -86,15 +86,28 @@ class OtpVerificationController extends GetxController { }, ); - if (response != 'failure' && response['status'] == 'success') { + if (response != 'failure') { Get.back(); // توجه إلى الصفحة التالية - Get.put(FirebaseMessagesController()).sendNotificationToDriverMAP( + await CRUD().post( + link: + '${AppLink.seferPaymentServer}/auth/token/update_driver_auth.php', + payload: { + 'token': box.read(BoxName.tokenDriver).toString(), + 'fingerPrint': finger.toString(), + 'captain_id': box.read(BoxName.driverID).toString(), + }); + final fcm = Get.isRegistered() + ? Get.find() + : Get.put(FirebaseMessagesController()); + + await fcm.sendNotificationToDriverMAP( 'token change', 'change device'.tr, ptoken.toString(), [], 'cancel.wav', ); + Get.offAll(() => HomeCaptain()); } else { Get.snackbar('Verification Failed', 'OTP is incorrect or expired'); diff --git a/lib/controller/auth/google_sign.dart b/lib/controller/auth/google_sign.dart index 87da615..4b7fd2d 100755 --- a/lib/controller/auth/google_sign.dart +++ b/lib/controller/auth/google_sign.dart @@ -94,22 +94,8 @@ class GoogleSignInHelper { static Future _handleSignOut() async { // Clear stored driver information - - box.remove(BoxName.driverID); - box.remove(BoxName.emailDriver); - box.remove(BoxName.lang); - box.remove(BoxName.nameDriver); - box.remove(BoxName.passengerID); - box.remove(BoxName.phoneDriver); - box.remove(BoxName.tokenFCM); - box.remove(BoxName.tokens); - box.remove(BoxName.carPlate); - box.remove(BoxName.lastNameDriver); - box.remove(BoxName.agreeTerms); - box.remove(BoxName.tokenDriver); - box.remove(BoxName.countryCode); - box.remove(BoxName.accountIdStripeConnect); - box.remove(BoxName.phoneVerified); + box.erase(); + storage.deleteAll(); Get.offAll(OnBoardingPage()); // Perform any additional sign-out tasks or API calls here // For example, you can notify your server about the user sign-out diff --git a/lib/controller/auth/syria/registration_controller.dart b/lib/controller/auth/syria/registration_controller.dart index 3d5ce57..8529253 100644 --- a/lib/controller/auth/syria/registration_controller.dart +++ b/lib/controller/auth/syria/registration_controller.dart @@ -1,18 +1,18 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; import 'package:image_cropper/image_cropper.dart'; import 'package:image/image.dart' as img; +import 'package:path/path.dart'; import 'package:sefer_driver/constant/links.dart'; import '../../../constant/box_name.dart'; import 'package:path_provider/path_provider.dart'; // --- Final Submission --- -import 'dart:convert'; -import 'dart:io'; +import 'package:path_provider/path_provider.dart' as path_provider; -import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:http_parser/http_parser.dart'; import 'package:mime/mime.dart'; @@ -21,8 +21,10 @@ import '../../../constant/colors.dart'; import '../../../constant/info.dart'; import '../../../main.dart'; import '../../../print.dart'; +import '../../../views/widgets/error_snakbar.dart'; import '../../functions/crud.dart'; import '../../functions/encrypt_decrypt.dart'; +import '../../functions/package_info.dart'; import '../captin/login_captin_controller.dart'; // You can create a simple enum to manage image types @@ -41,6 +43,11 @@ class RegistrationController extends GetxController { // Loading state var isLoading = false.obs; + var isloading = false; + CroppedFile? croppedFile; + final picker = ImagePicker(); + var image; + File? myImage; String? colorHex; // سيُملى من الدروب داون // Form Keys for validation final driverInfoFormKey = GlobalKey(); @@ -233,6 +240,142 @@ class RegistrationController extends GetxController { } } + /// خريطة لتخزين روابط المستندات بعد الرفع + final Map docUrls = { + 'driver_license_front': '', + 'driver_license_back': '', + 'car_license_front': '', + 'car_license_back': '', + }; + + /// التصرّف العام لاختيار/قص/ضغط/رفع الصورة حسب type + Future choosImage(String link, String imageType) async { + try { + final pickedImage = await picker.pickImage( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.rear, + ); + if (pickedImage == null) return; + + image = File(pickedImage.path); + + final croppedFile = await ImageCropper().cropImage( + sourcePath: image!.path, + uiSettings: [ + AndroidUiSettings( + toolbarTitle: 'Cropper'.tr, + toolbarColor: AppColor.blueColor, + toolbarWidgetColor: AppColor.yellowColor, + initAspectRatio: CropAspectRatioPreset.original, + lockAspectRatio: false, + ), + IOSUiSettings(title: 'Cropper'.tr), + ], + ); + if (croppedFile == null) return; + + // صورة للمعاينة داخل التطبيق + myImage = File(croppedFile.path); + + isloading = true; + update(); + + // ضغط (وأيضاً يمكنك إضافة rotateImageIfNeeded قبل/بعد الضغط إن رغبت) + final File compressedImage = await compressImage(File(croppedFile.path)); + + // تجهيز الحقول + final driverId = box.read(BoxName.driverID); + + final payload = { + 'driverID': driverId, + 'imageType': imageType, // مثال: driver_license_front + }; + + // الرفع وإرجاع الرابط + final String imageUrl = await uploadImage(compressedImage, payload, link); + + // حفظ الرابط محلياً حسب النوع + docUrls[imageType] = imageUrl; + + Log.print('✅ Uploaded $imageType => $imageUrl'); + } catch (e, st) { + Log.print('❌ Error in choosImage: $e\n$st'); + mySnackeBarError('Image Upload Failed'.tr); + } finally { + isloading = false; + update(); + } + } + + /// ترفع الملف وترجع رابط الصورة النهائي كـ String + Future uploadImage( + File file, Map data, String link) async { + final uri = Uri.parse(link); + final request = http.MultipartRequest('POST', uri); + + // الهيدرز (كما عندك) + final headers = { + 'Authorization': + 'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}', + 'X-HMAC-Auth': '${box.read(BoxName.hmac)}', + }; + request.headers.addAll(headers); + + // اسم الملف: driverID.jpg (اختياري) + final forcedName = '${box.read(BoxName.driverID) ?? 'image'}.jpg'; + + // إضافة الملف (من المسار مباشرة أسلم من الـ stream) + request.files.add( + await http.MultipartFile.fromPath( + 'image', // تأكد أنه نفس اسم الحقل على السيرفر + file.path, + filename: forcedName, + ), + ); + + // الحقول الإضافية + data.forEach((k, v) => request.fields[k] = v); + + // الإرسال + final streamed = await request.send(); + final res = await http.Response.fromStream(streamed); + + if (res.statusCode != 200) { + throw Exception( + 'Failed to upload image: ${res.statusCode} - ${res.body}'); + } + + // نحاول استخراج رابط الصورة من أكثر من مفتاح محتمل + final body = jsonDecode(res.body); + final String? url = body['url'] ?? + body['file_link'] ?? + body['image_url'] ?? + (body['data'] is Map ? body['data']['url'] : null); + + if (url == null || url.isEmpty) { + // لو السيرفر يرجع هيكل مختلف، عدّل هنا المفتاح حسب استجابتك الفعلية + throw Exception( + 'Upload succeeded but no image URL found in response: ${res.body}'); + } + + return url; + } + + Future compressImage(File file) async { + final dir = await path_provider.getTemporaryDirectory(); + final targetPath = "${dir.absolute.path}/temp.jpg"; + + var result = await FlutterImageCompress.compressAndGetFile( + file.absolute.path, + targetPath, + quality: 70, + minWidth: 1024, + minHeight: 1024, + ); + + return File(result!.path); + } + // دالة رفع إلى السيرفر السوري: ترجع file_url (Signed URL) Future uploadToSyria({ required String docType, @@ -317,30 +460,17 @@ class RegistrationController extends GetxController { } Future submitRegistration() async { - // 1) تحقق من الصور - if (driverLicenseFrontImage == null || - driverLicenseBackImage == null || - carLicenseFrontImage == null || - carLicenseBackImage == null) { - Get.snackbar( - 'Missing Documents'.tr, - 'Please upload all 4 required documents.'.tr, - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.orange, - colorText: Colors.white, - ); - return; - } + // 0) دوال/مساعدات محلية + + // 1) تحقق من وجود الروابط بدل الملفات + final driverFrontUrl = docUrls['driver_license_front']; + final driverBackUrl = docUrls['driver_license_back']; + final carFrontUrl = docUrls['car_license_front']; + final carBackUrl = docUrls['car_license_back']; isLoading.value = true; - // روابط الـ API - final registerUri = - Uri.parse(AppLink.register_driver_and_car); // التسجيل الرئيسي (PHP) - final syrianUploadUri = - // Uri.parse(AppLink.uploadSyrianDocs); // رفع الصور في سوريا - Uri.parse( - 'https://syria.intaleq.xyz/intaleq/auth/syria/uploadSyrianDocs.php'); // رفع الصور في سوريا + final registerUri = Uri.parse(AppLink.register_driver_and_car); final client = http.Client(); try { @@ -349,50 +479,7 @@ class RegistrationController extends GetxController { 'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}'; final hmac = '${box.read(BoxName.hmac)}'; - // 2) ارفع الصور أولاً على السيرفر السوري واحصل على روابطها (Signed URLs) - final driverId = (box.read(BoxName.driverID) ?? '').toString(); - - final driverFrontUrl = await uploadToSyria( - docType: 'driver_license_front', - file: driverLicenseFrontImage!, - syrianUploadUri: syrianUploadUri, - authHeader: bearer, - hmacHeader: hmac, - driverId: driverId, - clientOverride: client, - ); - - final driverBackUrl = await uploadToSyria( - docType: 'driver_license_back', - file: driverLicenseBackImage!, - syrianUploadUri: syrianUploadUri, - authHeader: bearer, - hmacHeader: hmac, - driverId: driverId, - clientOverride: client, - ); - - final carFrontUrl = await uploadToSyria( - docType: 'car_license_front', - file: carLicenseFrontImage!, - syrianUploadUri: syrianUploadUri, - authHeader: bearer, - hmacHeader: hmac, - driverId: driverId, - clientOverride: client, - ); - - final carBackUrl = await uploadToSyria( - docType: 'car_license_back', - file: carLicenseBackImage!, - syrianUploadUri: syrianUploadUri, - authHeader: bearer, - hmacHeader: hmac, - driverId: driverId, - clientOverride: client, - ); - - // 3) جهّز طلب التسجيل الرئيسي: نرسل الحقول + روابط الصور (لا نرفع الصور مرة ثانية) + // 2) جهّز طلب التسجيل الرئيسي: حقول فقط + روابط الصور (لا نرفع صور إطلاقًا) final req = http.MultipartRequest('POST', registerUri); req.headers.addAll({ 'Authorization': bearer, @@ -411,18 +498,16 @@ class RegistrationController extends GetxController { _addField( fields, 'password', 'generate_your_password_here'); // عدّل حسب منطقك _addField(fields, 'status', 'yet'); - _addField(fields, 'email', - 'Not specified'); // السيرفر سيحوّلها null ويبني ايميل افتراضي + _addField(fields, 'email', 'Not specified'); _addField(fields, 'gender', 'Male'); // --- Car Data --- - _addField(fields, 'vin', 'yet'); // تم تصحيح الاقتباس + _addField(fields, 'vin', 'yet'); _addField(fields, 'car_plate', carPlateController.text); _addField(fields, 'make', carMakeController.text); _addField(fields, 'model', carModelController.text); _addField(fields, 'year', carYearController.text); - _addField(fields, 'expiration_date', - driverLicenseExpiryController.text); // تم التصحيح + _addField(fields, 'expiration_date', driverLicenseExpiryController.text); _addField(fields, 'color', carColorController.text); _addField(fields, 'fuel', 'Gasoline'); if (colorHex != null && colorHex!.isNotEmpty) { @@ -431,32 +516,32 @@ class RegistrationController extends GetxController { _addField(fields, 'owner', '${firstNameController.text} ${lastNameController.text}'); - // --- روابط الصور الموقّعة من سوريا --- - _addField(fields, 'driver_license_front', driverFrontUrl); - _addField(fields, 'driver_license_back', driverBackUrl); - _addField(fields, 'car_license_front', carFrontUrl); - _addField(fields, 'car_license_back', carBackUrl); + // --- روابط الصور المخزنة مسبقًا --- + _addField(fields, 'driver_license_front', driverFrontUrl!); + _addField(fields, 'driver_license_back', driverBackUrl!); + _addField(fields, 'car_license_front', carFrontUrl!); + _addField(fields, 'car_license_back', carBackUrl!); // أضف الحقول req.fields.addAll(fields); - // 4) الإرسال + // 3) الإرسال final streamed = await client.send(req).timeout(const Duration(seconds: 60)); final resp = await http.Response.fromStream(streamed); - // 5) فحص النتيجة + // 4) فحص النتيجة Map? json; try { json = jsonDecode(resp.body) as Map; } catch (_) {} if (resp.statusCode == 200 && json?['status'] == 'success') { - final driverID = - (json!['data']?['driverID'] ?? json['driverID'])?.toString(); - if (driverID != null && driverID.isNotEmpty) { - box.write(BoxName.driverID, driverID); - } + // final driverID = + // (json!['data']?['driverID'] ?? json['driverID'])?.toString(); + // if (driverID != null && driverID.isNotEmpty) { + // box.write(BoxName.driverID, driverID); + // } Get.snackbar( 'Success'.tr, @@ -466,20 +551,31 @@ class RegistrationController extends GetxController { colorText: Colors.white, ); - // TODO: التنقّل أو تحديث الحالة… - final email = box.read(BoxName.emailDriver) ?? ''; - + // متابعة تسجيل الدخول إن لزم + final email = box.read(BoxName.emailDriver); + final driverID = box.read(BoxName.driverID); final c = Get.isRegistered() ? Get.find() : Get.put(LoginDriverController()); + //token to server + String fingerPrint = await DeviceHelper.getDeviceFingerprint(); + await CRUD().post(link: AppLink.addTokensDriver, payload: { + 'captain_id': (box.read(BoxName.driverID)).toString(), + 'token': (box.read(BoxName.tokenDriver)).toString(), + 'fingerPrint': fingerPrint.toString(), + }); + await CRUD().post(link: AppLink.addTokensDriverWallet, payload: { + 'token': box.read(BoxName.tokenDriver).toString(), + 'fingerPrint': fingerPrint.toString(), + 'captain_id': box.read(BoxName.driverID).toString(), + }); - c.loginWithGoogleCredential(driverId, email); + c.loginWithGoogleCredential(driverID, email); } else { final msg = (json?['message'] ?? 'Registration failed. Please try again.') .toString(); - Log.print('msg: ${msg}'); - + Log.print('msg: $msg'); Get.snackbar( 'Error'.tr, msg, @@ -500,8 +596,7 @@ class RegistrationController extends GetxController { client.close(); isLoading.value = false; } - } - // Future submitRegistration() async { + } // Future submitRegistration() async { // // 1) تحقق من الصور // if (driverLicenseFrontImage == null || // driverLicenseBackImage == null || diff --git a/lib/controller/functions/crud.dart b/lib/controller/functions/crud.dart index cc929f5..d4b80b8 100755 --- a/lib/controller/functions/crud.dart +++ b/lib/controller/functions/crud.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:jwt_decoder/jwt_decoder.dart'; import 'package:sefer_driver/controller/functions/network/net_guard.dart'; import 'package:secure_string_operations/secure_string_operations.dart'; @@ -16,6 +17,7 @@ import '../../constant/info.dart'; import '../../views/widgets/error_snakbar.dart'; import '../../print.dart'; import 'gemeni.dart'; +import 'network/connection_check.dart'; import 'upload_image.dart'; class CRUD { @@ -86,24 +88,27 @@ class CRUD { Map? payload, required Map headers, }) async { - // ✅ 1. Check for internet connection before making any request. - if (!await _netGuard.hasInternet(mustReach: Uri.parse(link))) { - // ✅ 2. If no internet, show a notification to the user (only once every 15s). - _netGuard.notifyOnce((title, msg) { - mySnackeBarError( - msg); // Using your existing snackbar for notifications. - }); - // ✅ 3. Return a specific status to indicate no internet. - return 'no_internet'; - } - - var url = Uri.parse(link); try { - var response = await http.post( - url, - body: payload, - headers: headers, + // 1. Wrap the http.post call directly with HttpRetry.sendWithRetry. + // It will attempt the request immediately and retry on transient errors. + var response = await HttpRetry.sendWithRetry( + () { + var url = Uri.parse(link); + return http.post( + url, + body: payload, + headers: headers, + ); + }, + // Optional: you can customize retry behavior for each call + maxRetries: 3, + timeout: const Duration(seconds: 15), ); + // Log.print('response: ${response.body}'); + // Log.print('request: ${response.request}'); + // Log.print('payload: ${payload}'); + // ✅ All your existing logic for handling server responses remains the same. + // This part is only reached if the network request itself was successful. // Handle successful response (200 OK) if (response.statusCode == 200) { @@ -112,16 +117,18 @@ class CRUD { if (jsonData['status'] == 'success') { return jsonData; // Return the full JSON object on success } else { - // The API reported a logical failure (e.g., validation error) - addError( - 'API Logic Error: ${jsonData['status']}', - 'Response: ${response.body}', - 'CRUD._makeRequest - $link', - ); + if (jsonData['status'] == 'failure') { + // return 'failure'; + } else { + addError( + 'API Logic Error: ${jsonData['status']}', + 'Response: ${response.body}', + 'CRUD._makeRequest - $link', + ); + } return jsonData['status']; // Return the specific status string } } catch (e, stackTrace) { - // Error decoding the JSON response from the server addError( 'JSON Decode Error: $e', 'Response Body: ${response.body}\nStack Trace: $stackTrace', @@ -130,20 +137,13 @@ class CRUD { return 'failure'; } } - // Handle Unauthorized (401) - typically means token expired + // Handle Unauthorized (401) else if (response.statusCode == 401) { var jsonData = jsonDecode(response.body); if (jsonData['error'] == 'Token expired') { - // The token refresh logic is handled before the call, - // but we log this case if it still happens. - // addError( - // 'Token Expired', - // 'A new token should have been fetched before this call.', - // 'CRUD._makeRequest - $link', - // ); + await Get.put(LoginDriverController()).getJWT(); return 'token_expired'; } else { - // Other 401 errors (e.g., invalid token) addError( 'Unauthorized Error: ${jsonData['error']}', 'Status Code: 401', @@ -161,8 +161,14 @@ class CRUD { ); return 'failure'; } + } on SocketException { + // 2. This block now catches the "no internet" case after all retries have failed. + _netGuard.notifyOnce((title, msg) { + mySnackeBarError(msg); + }); + return 'no_internet'; // Return the specific status you were using before. } catch (e, stackTrace) { - // Handle network exceptions (e.g., no internet, DNS error) + // 3. This is a general catch-all for any other unexpected errors. addError( 'HTTP Request Exception: $e', 'Stack Trace: $stackTrace', @@ -177,15 +183,15 @@ class CRUD { Map? payload, }) async { // 1. Check if the token is expired - bool isTokenExpired = JwtDecoder.isExpired(X - .r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs) - .toString() - .split(AppInformation.addd)[0]); + // bool isTokenExpired = JwtDecoder.isExpired(X + // .r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs) + // .toString() + // .split(AppInformation.addd)[0]); - // 2. If expired, get a new one - if (isTokenExpired) { - await LoginDriverController().getJWT(); - } + // // 2. If expired, get a new one + // if (isTokenExpired) { + // await LoginDriverController().getJWT(); + // } // 3. Prepare the headers with the valid token final headers = { @@ -303,15 +309,15 @@ class CRUD { required String link, Map? payload, }) async { - bool isTokenExpired = JwtDecoder.isExpired(X - .r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs) - .toString() - .split(AppInformation.addd)[0]); - // Log.print('isTokenExpired: ${isTokenExpired}'); + // bool isTokenExpired = JwtDecoder.isExpired(X + // .r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs) + // .toString() + // .split(AppInformation.addd)[0]); + // // Log.print('isTokenExpired: ${isTokenExpired}'); - if (isTokenExpired) { - await LoginDriverController().getJWT(); - } + // if (isTokenExpired) { + // await LoginDriverController().getJWT(); + // } // await Get.put(LoginDriverController()).getJWT(); var url = Uri.parse( link, diff --git a/lib/controller/functions/location_controller.dart b/lib/controller/functions/location_controller.dart index 995e297..9203038 100755 --- a/lib/controller/functions/location_controller.dart +++ b/lib/controller/functions/location_controller.dart @@ -109,8 +109,7 @@ class LocationController extends GetxController { // ✅ تحديث للسيرفر await CRUD().post( - link: - box.read(BoxName.serverChosen) + '/ride/location/update.php', + link: '${AppLink.server}/ride/location/update.php', payload: payload, ); @@ -141,7 +140,7 @@ class LocationController extends GetxController { if (_insertCounter == 12) { _insertCounter = 0; await CRUD().post( - link: box.read(BoxName.serverChosen) + '/ride/location/add.php', + link: '${AppLink.server}/ride/location/add.php', payload: payload, ); } diff --git a/lib/controller/functions/overlay_permisssion.dart b/lib/controller/functions/overlay_permisssion.dart index 7e79057..a086d27 100755 --- a/lib/controller/functions/overlay_permisssion.dart +++ b/lib/controller/functions/overlay_permisssion.dart @@ -35,13 +35,13 @@ Future showDriverGiftClaim(BuildContext context) async { if (box.read(BoxName.is_claimed).toString() == '0' || box.read(BoxName.is_claimed) == null) { MyDialog().getDialog( - 'You have gift 300 L.E'.tr, 'This for new registration'.tr, () async { + 'You have gift 30000 SYP'.tr, 'This for new registration'.tr, () async { var res = await CRUD().post(link: AppLink.updateDriverClaim, payload: { 'driverId': box.read(BoxName.driverID), }); if (res != 'failure') { Get.find() - .addDriverWallet('new driver', '300', '300'); + .addDriverWallet('new driver', '30000', '30000'); Confetti.launch( context, options: diff --git a/lib/controller/functions/sms_egypt_controller.dart b/lib/controller/functions/sms_egypt_controller.dart index b85c30e..1ae9f2c 100755 --- a/lib/controller/functions/sms_egypt_controller.dart +++ b/lib/controller/functions/sms_egypt_controller.dart @@ -77,7 +77,7 @@ class SmsEgyptController extends GetxController { var res = await http.post( Uri.parse(AppLink.checkCredit), body: { - "username": AppInformation.appName, + "username": 'Sefer', "password": AK.smsPasswordEgypt, "message": "This is an example SMS message.", "language": box.read(BoxName.lang) == 'en' ? "e" : 'r', diff --git a/lib/controller/home/captin/home_captain_controller.dart b/lib/controller/home/captin/home_captain_controller.dart index 5d0bf2f..6af4a3c 100755 --- a/lib/controller/home/captin/home_captain_controller.dart +++ b/lib/controller/home/captin/home_captain_controller.dart @@ -426,9 +426,14 @@ class HomeCaptainController extends GetxController { var res = await CRUD().get( link: AppLink.getAllPaymentFromRide, payload: {'driverID': box.read(BoxName.driverID).toString()}); - data = jsonDecode(res); + if (res == 'failure') { + totalMoneyInSEFER = '0'; + } else { + data = jsonDecode(res); + + totalMoneyInSEFER = data['message'][0]['total_amount']; + } - totalMoneyInSEFER = data['message'][0]['total_amount'] ?? '0'; update(); } diff --git a/lib/controller/home/captin/map_driver_controller.dart b/lib/controller/home/captin/map_driver_controller.dart index 1387d23..e649ea4 100755 --- a/lib/controller/home/captin/map_driver_controller.dart +++ b/lib/controller/home/captin/map_driver_controller.dart @@ -331,13 +331,7 @@ class MapDriverController extends GetxController { 'driverGoToPassengerTime': DateTime.now().toString(), 'status': 'Applied' }); - if (AppLink.endPoint != AppLink.seferCairoServer) { - CRUD().post(link: "${AppLink.endPoint}/ride/rides/update.php", payload: { - 'id': (rideId), - 'driverGoToPassengerTime': DateTime.now().toString(), - 'status': 'Applied' - }); - } + // Get.find().changeToAppliedRide('Applied'); Get.find().sendNotificationToDriverMAP( diff --git a/lib/controller/home/captin/navigation_service.dart b/lib/controller/home/captin/navigation_service.dart new file mode 100644 index 0000000..83120aa --- /dev/null +++ b/lib/controller/home/captin/navigation_service.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_polyline_algorithm/google_polyline_algorithm.dart'; +import 'package:sefer_driver/constant/api_key.dart'; +import 'package:sefer_driver/constant/box_name.dart'; +import 'package:sefer_driver/constant/links.dart'; +import 'package:sefer_driver/controller/functions/crud.dart'; +import 'package:sefer_driver/controller/functions/tts.dart'; + +import '../../../main.dart'; + +/// Handles map-related logic: fetching routes, drawing polylines, and managing markers. +class NavigationService extends GetxService { + final CRUD _crud = CRUD(); + final TextToSpeechController _tts = Get.put(TextToSpeechController()); + + final RxSet markers = {}.obs; + final RxSet polylines = {}.obs; + final RxString currentInstruction = "".obs; + + BitmapDescriptor carIcon = BitmapDescriptor.defaultMarker; + BitmapDescriptor passengerIcon = BitmapDescriptor.defaultMarker; + BitmapDescriptor startIcon = BitmapDescriptor.defaultMarker; + BitmapDescriptor endIcon = BitmapDescriptor.defaultMarker; + + @override + void onInit() { + super.onInit(); + _loadCustomIcons(); + } + + void _loadCustomIcons() async { + carIcon = await _createBitmapDescriptor('assets/images/car.png'); + passengerIcon = await _createBitmapDescriptor('assets/images/picker.png'); + startIcon = await _createBitmapDescriptor('assets/images/A.png'); + endIcon = await _createBitmapDescriptor('assets/images/b.png'); + } + + Future _createBitmapDescriptor(String assetName) { + return BitmapDescriptor.fromAssetImage( + ImageConfiguration( + size: const Size(30, 35), devicePixelRatio: Get.pixelRatio), + assetName, + ); + } + + Future?> getRoute({ + required LatLng origin, + required LatLng destination, + }) async { + final url = + '${AppLink.googleMapsLink}directions/json?language=${box.read(BoxName.lang)}&destination=${destination.latitude},${destination.longitude}&origin=${origin.latitude},${origin.longitude}&key=${AK.mapAPIKEY}'; + + final response = await _crud.getGoogleApi(link: url, payload: {}); + + if (response != null && response['routes'].isNotEmpty) { + return response['routes'][0]; + } + return null; + } + + void drawRoute(Map routeData, {Color color = Colors.blue}) { + final pointsString = routeData["overview_polyline"]["points"]; + final points = decodePolyline(pointsString) + .map((p) => LatLng(p[0].toDouble(), p[1].toDouble())) + .toList(); + + final polyline = Polyline( + polylineId: PolylineId(routeData["summary"] ?? DateTime.now().toString()), + points: points, + width: 8, + color: color, + ); + + polylines.add(polyline); + } + + void updateCarMarker(LatLng position, double heading) { + markers.removeWhere((m) => m.markerId.value == 'MyLocation'); + markers.add( + Marker( + markerId: MarkerId('MyLocation'.tr), + position: position, + icon: carIcon, + rotation: heading, + anchor: const Offset(0.5, 0.5), + flat: true, + ), + ); + } + + void setInitialMarkers( + LatLng passengerLocation, LatLng passengerDestination) { + markers.clear(); + markers.add(Marker( + markerId: const MarkerId('passengerLocation'), + position: passengerLocation, + icon: passengerIcon, + )); + markers.add(Marker( + markerId: const MarkerId('passengerDestination'), + position: passengerDestination, + icon: endIcon, + )); + } + + void clearRoutes() { + polylines.clear(); + currentInstruction.value = ""; + } +} diff --git a/lib/controller/home/navigation/navigation_controller.dart b/lib/controller/home/navigation/navigation_controller.dart index 2429160..6dd569a 100644 --- a/lib/controller/home/navigation/navigation_controller.dart +++ b/lib/controller/home/navigation/navigation_controller.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:math'; +import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; @@ -10,6 +11,7 @@ import 'package:sefer_driver/constant/colors.dart'; // استخدام نفس مسارات الاستيراد التي قدمتها import '../../../constant/api_key.dart'; import '../../../constant/links.dart'; +import '../../../print.dart'; import '../../functions/crud.dart'; import '../../functions/tts.dart'; @@ -525,45 +527,127 @@ class NavigationController extends GetxController { String _parseInstruction(String html) => html.replaceAll(RegExp(r'<[^>]*>'), ' '); + double _haversineKm(double lat1, double lon1, double lat2, double lon2) { + const R = 6371.0; // km + final dLat = (lat2 - lat1) * math.pi / 180.0; + final dLon = (lon2 - lon1) * math.pi / 180.0; + final a = math.sin(dLat / 2) * math.sin(dLat / 2) + + math.cos(lat1 * math.pi / 180.0) * + math.cos(lat2 * math.pi / 180.0) * + math.sin(dLon / 2) * + math.sin(dLon / 2); + final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)); + return R * c; + } + + /// تحويل نصف قطر بالكيلومتر إلى دلتا درجات عرض + double _kmToLatDelta(double km) => km / 111.0; + + /// تحويل نصف قطر بالكيلومتر إلى دلتا درجات طول (تعتمد على خط العرض) + double _kmToLngDelta(double km, double atLat) => + km / (111.320 * math.cos(atLat * math.pi / 180.0)).abs().clamp(1e-6, 1e9); + + /// حساب درجة التطابق النصي (كل كلمة تبدأ بها الاسم = 2 نقاط، يحتويها = 1 نقطة) + double _relevanceScore(String name, String query) { + final n = name.toLowerCase(); + final parts = + query.toLowerCase().split(RegExp(r'\s+')).where((p) => p.length >= 2); + double s = 0.0; + for (final p in parts) { + if (n.startsWith(p)) { + s += 2.0; + } else if (n.contains(p)) { + s += 1.0; + } + } + return s; + } + Future getPlaces() async { - if (placeDestinationController.text.trim().isEmpty) { + final q = placeDestinationController.text.trim(); + if (q.isEmpty) { placesDestination = []; update(); return; } - if (myLocation == null) { - Get.snackbar('انتظر', 'جاري تحديد موقعك الحالي...'); - return; - } - final query = placeDestinationController.text.trim(); + final lat = myLocation!.latitude; final lng = myLocation!.longitude; - const double range = 2.2; - final lat_min = lat - range, - lat_max = lat + range, - lng_min = lng - range, - lng_max = lng + range; + + // نصف قطر البحث بالكيلومتر (عدّل حسب رغبتك) + const radiusKm = 200.0; + + // حساب الباوند الصحيح (درجات، وليس 2.2 درجة ثابتة) + final latDelta = _kmToLatDelta(radiusKm); + final lngDelta = _kmToLngDelta(radiusKm, lat); + + final latMin = lat - latDelta; + final latMax = lat + latDelta; + final lngMin = lng - lngDelta; + final lngMax = lng + lngDelta; try { final response = await CRUD().post( link: AppLink.getPlacesSyria, payload: { - 'query': query, - 'lat_min': lat_min.toString(), - 'lat_max': lat_max.toString(), - 'lng_min': lng_min.toString(), - 'lng_max': lng_max.toString(), + 'query': q, + 'lat_min': latMin.toString(), + 'lat_max': latMax.toString(), + 'lng_min': lngMin.toString(), + 'lng_max': lngMax.toString(), }, ); - if (response != 'failure') { - placesDestination = response['message'] ?? []; + + // يدعم شكلي استجابة: إما {"...","message":[...]} أو قائمة مباشرة [...] + List list; + if (response is Map && response['message'] is List) { + list = List.from(response['message'] as List); + } else if (response is List) { + list = List.from(response); } else { - placesDestination = []; + print('Unexpected response shape'); + return; } + + // جهّز الحقول المحتملة للأسماء + String _bestName(Map p) { + return (p['name'] ?? p['name_ar'] ?? p['name_en'] ?? '').toString(); + } + + // احسب المسافة ودرجة التطابق والنقاط + for (final p in list) { + final plat = double.tryParse(p['latitude']?.toString() ?? '') ?? 0.0; + final plng = double.tryParse(p['longitude']?.toString() ?? '') ?? 0.0; + + final d = _haversineKm(lat, lng, plat, plng); + final rel = _relevanceScore(_bestName(p), q); + + // معادلة ترتيب ذكية: مسافة أقل + تطابق أعلى = نقاط أعلى + // تضيف +1 لضمان عدم وصول الوزن للصفر عند عدم وجود تطابق + final score = (1.0 / (1.0 + d)) * (1.0 + rel); + + p['distanceKm'] = d; + p['relevance'] = rel; + p['score'] = score; + } + + // رتّب حسب score تنازليًا، ثم المسافة تصاعديًا كحسم + list.sort((a, b) { + final sa = (a['score'] ?? 0.0) as double; + final sb = (b['score'] ?? 0.0) as double; + final cmp = sb.compareTo(sa); + if (cmp != 0) return cmp; + final da = (a['distanceKm'] ?? 1e9) as double; + final db = (b['distanceKm'] ?? 1e9) as double; + return da.compareTo(db); + }); + + // خذ أول 10–15 للعرض (اختياري)، أو اعرض الكل + placesDestination = list.take(15).toList(); + Log.print('placesDestination: $placesDestination'); + update(); } catch (e) { print('Exception in getPlaces: $e'); - } finally { - update(); } } diff --git a/lib/controller/home/payment/paymob_payout.dart b/lib/controller/home/payment/paymob_payout.dart index 64bb3dd..f29e9e1 100755 --- a/lib/controller/home/payment/paymob_payout.dart +++ b/lib/controller/home/payment/paymob_payout.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:local_auth/local_auth.dart'; import 'package:sefer_driver/constant/box_name.dart'; +import 'package:sefer_driver/controller/payment/smsPaymnet/payment_services.dart'; import 'package:sefer_driver/main.dart'; import 'package:sefer_driver/views/widgets/error_snakbar.dart'; import 'package:get/get.dart'; @@ -27,48 +28,48 @@ class PaymobPayout extends GetxController { sensitiveTransaction: true, )); if (didAuthenticate) { - var dec = await CRUD() - .postWallet(link: AppLink.paymobPayoutDriverWallet, payload: { - "issuer": issuer, - "method": "wallet", - "amount": amount, //9.0, - "full_name": - '${box.read(BoxName.nameDriver)} ${box.read(BoxName.lastNameDriver)}', - "msisdn": msisdn, //"01010101010", - "bank_transaction_type": "cash_transfer" - }); - if (dec['disbursement_status'] == 'successful') { - var paymentToken = await Get.find() - .generateToken( - ((-1) * (double.parse(dec['amount'].toString())) - payOutFee) - .toStringAsFixed(0)); - await CRUD().postWallet(link: AppLink.addDrivePayment, payload: { - 'rideId': DateTime.now().toIso8601String(), - 'amount': - ((-1) * (double.parse(dec['amount'].toString())) - payOutFee) - .toStringAsFixed(0), - 'payment_method': 'payout', - 'passengerID': 'myself', - 'token': paymentToken, - 'driverID': box.read(BoxName.driverID).toString(), - }); - await Get.find() - .addSeferWallet('payout fee myself', payOutFee.toString()); - await updatePaymentToPaid(box.read(BoxName.driverID).toString()); - await sendEmail( - box.read(BoxName.driverID).toString(), - amount, - box.read(BoxName.phoneDriver).toString(), - box.read(BoxName.nameDriver).toString(), - 'Wallet', - box.read(BoxName.emailDriver).toString()); + // var dec = await CRUD() + // .postWallet(link: AppLink.paymobPayoutDriverWallet, payload: { + // "issuer": issuer, + // "method": "wallet", + // "amount": amount, //9.0, + // "full_name": + // '${box.read(BoxName.nameDriver)} ${box.read(BoxName.lastNameDriver)}', + // "msisdn": msisdn, //"01010101010", + // "bank_transaction_type": "cash_transfer" + // }); + // if (dec['disbursement_status'] == 'successful') { + // var paymentToken = await Get.find() + // .generateToken( + // ((-1) * (double.parse(dec['amount'].toString())) - payOutFee) + // .toStringAsFixed(0)); + // await CRUD().postWallet(link: AppLink.addDrivePayment, payload: { + // 'rideId': DateTime.now().toIso8601String(), + // 'amount': + // ((-1) * (double.parse(dec['amount'].toString())) - payOutFee) + // .toStringAsFixed(0), + // 'payment_method': 'payout', + // 'passengerID': 'myself', + // 'token': paymentToken, + // 'driverID': box.read(BoxName.driverID).toString(), + // }); + // await Get.find() + // .addSeferWallet('payout fee myself', payOutFee.toString()); + // await updatePaymentToPaid(box.read(BoxName.driverID).toString()); + // await sendEmail( + // box.read(BoxName.driverID).toString(), + // amount, + // box.read(BoxName.phoneDriver).toString(), + // box.read(BoxName.nameDriver).toString(), + // 'Wallet', + // box.read(BoxName.emailDriver).toString()); - mySnackbarSuccess('${'Transaction successful'.tr} ${dec['amount']}'); + // mySnackbarSuccess('${'Transaction successful'.tr} ${dec['amount']}'); - Get.find().refreshCaptainWallet(); - } else if (dec['disbursement_status'] == 'failed') { - mySnackeBarError('Transaction failed'.tr); - } + // Get.find().refreshCaptainWallet(); + // } else if (dec['disbursement_status'] == 'failed') { + // mySnackeBarError('Transaction failed'.tr); + // } } else { MyDialog().getDialog('Authentication failed'.tr, ''.tr, () { Get.back(); diff --git a/lib/controller/home/splash_screen_controlle.dart b/lib/controller/home/splash_screen_controlle.dart index 12770e0..52ad424 100755 --- a/lib/controller/home/splash_screen_controlle.dart +++ b/lib/controller/home/splash_screen_controlle.dart @@ -63,7 +63,7 @@ class SplashScreenController extends GetxController box.read(BoxName.onBoarding) == null ? Get.off(() => OnBoardingPage()) : box.read(BoxName.phoneDriver) != null && - box.read(BoxName.phoneVerified) == '1' + box.read(BoxName.phoneVerified).toString() == '1' ? await Get.put(LoginDriverController()) .loginWithGoogleCredential( box.read(BoxName.driverID).toString(), diff --git a/lib/controller/local/translations.dart b/lib/controller/local/translations.dart index 741aaa9..4ab672f 100755 --- a/lib/controller/local/translations.dart +++ b/lib/controller/local/translations.dart @@ -35,6 +35,8 @@ class MyTranslation extends Translations { "نأسف لإعلامك بأن سائقًا آخر قد قبل هذا الطلب.", "Driver Applied the Ride for You": "السائق قدم الطلب لك", "Applied": "تم التقديم", + 'Pay by Sham Cash': 'الدفع عبر شام كاش', + 'Pay with Debit Card': 'الدفع ببطاقة الخصم', "Please go to Car Driver": "يرجى الذهاب إلى سائق السيارة", "Ok I will go now.": "حسنًا، سأذهب الآن.", "Accepted Ride": "تم قبول الرحلة", @@ -84,6 +86,8 @@ class MyTranslation extends Translations { "Password must be at least 6 characters": "يجب أن تتكون كلمة المرور من 6 أحرف على الأقل", "Create Account": "إنشاء حساب", + 'Pay by MTN Wallet': 'الدفع عبر محفظة MTN', + 'Pay by Syriatel Wallet': 'الدفع عبر محفظة سيريتل', "Login": "تسجيل الدخول", "Back to other sign-in options": "العودة إلى خيارات التسجيل الأخرى", "Driver Agreement": "اتفاقية السائق", @@ -93,7 +97,11 @@ class MyTranslation extends Translations { " and acknowledge our Privacy Policy.": " والإقرار بسياسة الخصوصية الخاصة بنا.", "I Agree": "أنا أوافق", - "Continue": "متابعة", + "Continue": "متابعة", "Customer not found": "العميل غير موجود", + "Wallet is blocked": "المحفظة محظورة", + "Customer phone is not active": "هاتف العميل غير نشط", + "Balance not enough": "الرصيد غير كافٍ", + "Balance limit exceeded": "تم تجاوز حد الرصيد", "Privacy Policy": "سياسة الخصوصية", "Location Access Required": "مطلوب الوصول إلى الموقع", "We need access to your location to match you with nearby passengers and provide accurate navigation.": @@ -383,6 +391,7 @@ Raih Gai: For same-day return trips longer than 50km. "Driver's Personal Information": "المعلومات الشخصية للسائق", "First Name": "الاسم الأول", "Last Name": "اسم العائلة", + 'You have gift 30000 SYP': 'لديك هدية 30000 ليرة سورية', "National ID Number": "الرقم الوطني", "License Expiry Date": "تاريخ انتهاء الرخصة", "YYYY-MM-DD": "YYYY-MM-DD", diff --git a/lib/controller/payment/smsPaymnet/pay_out_syria_controller.dart b/lib/controller/payment/smsPaymnet/pay_out_syria_controller.dart new file mode 100644 index 0000000..c5d7c71 --- /dev/null +++ b/lib/controller/payment/smsPaymnet/pay_out_syria_controller.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:sefer_driver/controller/functions/crud.dart'; + +class PayoutService { + final String _baseUrl = + "https://walletintaleq.intaleq.xyz/v1/main/sms_webhook"; + static const double payoutFee = 5000.0; // عمولة السحب الثابتة + + /// دالة لإنشاء طلب سحب جديد على السيرفر + /// + /// تعيد رسالة النجاح من السيرفر، أو رسالة خطأ في حال الفشل. + Future requestPayout({ + required String driverId, + walletType, + payoutPhoneNumber, + required double amount, + }) async { + final url = ("$_baseUrl/request_payout.php"); + try { + // هنا يمكنك إضافة هيدرز المصادقة (JWT) بنفس طريقتك المعتادة + final response = await CRUD().postWallet(link: url, payload: { + 'driverId': driverId, + 'amount': amount.toString(), + 'phone': payoutPhoneNumber.toString(), + 'wallet_type': walletType.toString(), + }).timeout(const Duration(seconds: 20)); + + if (response != 'failure') { + final data = (response); + if (data['status'] == 'success') { + debugPrint("Payout request successful: ${data['message']}"); + return data['message']; // إرجاع رسالة النجاح + } else { + debugPrint("Payout request failed: ${data['message']}"); + return "فشل الطلب: ${data['message']}"; // إرجاع رسالة الخطأ من السيرفر + } + } else { + return "خطأ في الاتصال بالسيرفر: ${response.statusCode}"; + } + } catch (e) { + debugPrint("Exception during payout request: $e"); + return "حدث خطأ غير متوقع. يرجى المحاولة مرة أخرى."; + } + } +} diff --git a/lib/controller/payment/smsPaymnet/payment_services.dart b/lib/controller/payment/smsPaymnet/payment_services.dart new file mode 100644 index 0000000..1b37802 --- /dev/null +++ b/lib/controller/payment/smsPaymnet/payment_services.dart @@ -0,0 +1,397 @@ +// لإضافة هذه الحزمة، قم بتشغيل الأمر التالي في الـ Terminal +// flutter pub add intl + +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:http/http.dart' as http; +import 'package:intl/intl.dart'; +import 'package:sefer_driver/constant/links.dart'; +import 'package:sefer_driver/controller/functions/crud.dart'; + +import '../../../constant/box_name.dart'; +import '../../../main.dart'; + +/// خدمة لإدارة عمليات الدفع المتعلقة بنظام الدفع عبر الرسائل القصيرة +class PaymentService { + final String _baseUrl = "${AppLink.seferPaymentServer}/sms_webhook"; + + Future createInvoice({ + required String userPhone, + required double amount, + }) async { + final url = "$_baseUrl/create_invoice.php"; + try { + final response = await CRUD().postWallet( + link: url, + payload: { + 'user_phone': userPhone.toString(), + 'driverID': box.read(BoxName.driverID), + 'amount': amount.toString(), + }, + ).timeout(const Duration(seconds: 15)); // إضافة مهلة للطلب + + if (response != 'failure') { + final data = (response); + if (data['status'] == 'success' && data['invoice_number'] != null) { + debugPrint( + "تم إنشاء الفاتورة بنجاح. الرقم: ${data['invoice_number']}"); + return data['invoice_number'].toString(); + } else { + debugPrint("فشل في إنشاء الفاتورة من السيرفر: ${data['message']}"); + return null; + } + } else { + debugPrint("خطأ في السيرفر عند إنشاء الفاتورة: ${response.statusCode}"); + return null; + } + } catch (e) { + debugPrint("حدث استثناء عند إنشاء الفاتورة: $e"); + return null; + } + } + + /// دالة للتحقق من حالة فاتورة واحدة + Future checkInvoiceStatus(String invoiceNumber) async { + final url = "$_baseUrl/check_invoice_status.php"; + try { + final response = await CRUD().postWallet(link: url, payload: { + 'invoice_number': invoiceNumber, + }).timeout(const Duration(seconds: 10)); // مهلة للشبكة + + if (response != 'failure') { + final data = (response); + return data['status'] == 'success' && + data['invoice_status'] == 'completed'; + } + return false; + } catch (e) { + debugPrint("خطأ أثناء التحقق من الفاتورة: $e"); + return false; + } + } +} + +enum PaymentStatus { + creatingInvoice, + waitingForPayment, + paymentSuccess, + paymentTimeout, + paymentError +} + +class PaymentScreenSmsProvider extends StatefulWidget { + final double amount; + final String providerName; + final String providerLogo; + final String paymentPhoneNumber; + + const PaymentScreenSmsProvider({ + super.key, + required this.amount, + this.providerName = 'شام كاش', + this.providerLogo = 'assets/images/shamCash.png', + this.paymentPhoneNumber = '963942542053', + }); + + @override + _PaymentScreenSmsProviderState createState() => + _PaymentScreenSmsProviderState(); +} + +class _PaymentScreenSmsProviderState extends State { + final PaymentService _paymentService = PaymentService(); + Timer? _pollingTimer; + PaymentStatus _status = PaymentStatus.creatingInvoice; + String? _invoiceNumber; + final String phone = box.read(BoxName.phoneWallet); + + @override + void initState() { + super.initState(); + _createAndPollInvoice(); + } + + @override + void dispose() { + _pollingTimer?.cancel(); // مهم جداً: إلغاء المؤقت عند الخروج من الشاشة + super.dispose(); + } + + void _createAndPollInvoice() async { + setState(() => _status = PaymentStatus.creatingInvoice); + + final invoiceNumber = await _paymentService.createInvoice( + userPhone: phone, + amount: widget.amount, + ); + + if (invoiceNumber != null && mounted) { + setState(() { + _invoiceNumber = invoiceNumber; + _status = PaymentStatus.waitingForPayment; + }); + _startPolling(invoiceNumber); + } else if (mounted) { + setState(() => _status = PaymentStatus.paymentError); + } + } + + void _startPolling(String invoiceNumber) { + const timeoutDuration = Duration(minutes: 3); + var elapsed = Duration.zero; + + _pollingTimer = Timer.periodic(const Duration(seconds: 5), (timer) async { + elapsed += const Duration(seconds: 5); + if (elapsed >= timeoutDuration) { + timer.cancel(); + if (mounted) setState(() => _status = PaymentStatus.paymentTimeout); + return; + } + + debugPrint("Polling... Checking invoice status for: $invoiceNumber"); + final isCompleted = + await _paymentService.checkInvoiceStatus(invoiceNumber); + if (isCompleted && mounted) { + timer.cancel(); + setState(() => _status = PaymentStatus.paymentSuccess); + // TODO: تحديث رصيد المستخدم أو تنفيذ الإجراءات اللازمة + } + }); + } + + /// دالة جديدة لمعالجة محاولة الرجوع للخلف + void _onPopInvoked(bool didPop) async { + // إذا كان الرجوع قد تم بالفعل (مثلاً من خلال Navigator.pop)، لا تفعل شيئاً + if (didPop) return; + + // إذا كان المستخدم ينتظر الدفع، أظهر له حوار التأكيد + if (_status == PaymentStatus.waitingForPayment) { + final shouldPop = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('هل أنت متأكد؟'), + content: const Text('إذا خرجت الآن، سيتم إلغاء عملية الدفع الحالية.'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('البقاء'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text('الخروج'), + ), + ], + ), + ); + + // إذا وافق المستخدم على الخروج، قم بإغلاق الشاشة + if (shouldPop ?? false) { + Navigator.of(context).pop(); + } + } + } + + @override + Widget build(BuildContext context) { + // استخدام PopScope بدلاً من WillPopScope + return PopScope( + // منع الرجوع التلقائي فقط في حالة انتظار الدفع + canPop: _status != PaymentStatus.waitingForPayment, + // استدعاء دالة التحقق عند محاولة الرجوع + onPopInvoked: _onPopInvoked, + child: Scaffold( + appBar: AppBar(title: Text("الدفع عبر ${widget.providerName}")), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Center( + child: _buildContentByStatus(), + ), + ), + ), + ); + } + + Widget _buildContentByStatus() { + switch (_status) { + case PaymentStatus.creatingInvoice: + return const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 20), + Text("جاري إنشاء فاتورة الدفع...", style: TextStyle(fontSize: 16)), + ], + ); + case PaymentStatus.waitingForPayment: + return _buildWaitingForPaymentUI(); + case PaymentStatus.paymentSuccess: + return _buildSuccessUI(); + case PaymentStatus.paymentTimeout: + case PaymentStatus.paymentError: + return _buildErrorUI(); + } + } + + Widget _buildWaitingForPaymentUI() { + final currencyFormat = NumberFormat.decimalPattern('ar_SY'); + final invoiceText = _invoiceNumber ?? '------'; + + return SingleChildScrollView( + child: Column( + children: [ + Image.asset(widget.providerLogo, width: 96), + const SizedBox(height: 16), + Text("تعليمات الدفع", style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 12), + Card( + elevation: 1.5, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + _StepTile(number: 1, text: "افتح تطبيق محفظتك الإلكترونية."), + _StepTile(number: 2, text: "اختر خدمة تحويل الأموال."), + _StepTile( + number: 3, + text: + "أدخل المبلغ المطلوب: ${currencyFormat.format(widget.amount)} ل.س"), + _StepTile(number: 4, text: "حوّل إلى الرقم التالي:"), + // --- التعديل هنا --- + ListTile( + contentPadding: EdgeInsets.zero, + title: Text( + widget.paymentPhoneNumber, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + letterSpacing: 1.2), + ), + trailing: OutlinedButton.icon( + onPressed: () async { + await Clipboard.setData( + ClipboardData(text: widget.paymentPhoneNumber)); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("تم نسخ رقم الهاتف"))); + } + }, + icon: const Icon(Icons.copy, size: 18), + label: const Text("نسخ"), + ), + ), + // --- نهاية التعديل --- + const SizedBox(height: 8), + _StepTile( + number: 5, + text: "هام: انسخ رقم القسيمة والصقه في خانة \"البيان\"."), + ListTile( + contentPadding: EdgeInsets.zero, + title: Text(invoiceText, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + letterSpacing: 1.5)), + trailing: OutlinedButton.icon( + onPressed: _invoiceNumber == null + ? null + : () async { + await Clipboard.setData( + ClipboardData(text: invoiceText)); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("تم نسخ رقم القسيمة"))); + } + }, + icon: const Icon(Icons.copy, size: 18), + label: const Text("نسخ"), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 20), + const LinearProgressIndicator(minHeight: 2), + const SizedBox(height: 12), + Text("بانتظار تأكيد الدفع...", + style: TextStyle(color: Colors.grey.shade700)), + const SizedBox(height: 4), + const Text("هذه الشاشة ستتحدث تلقائيًا", + style: TextStyle(color: Colors.grey)), + ], + ), + ); + } + + Widget _buildSuccessUI() { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.check_circle, color: Colors.green, size: 80), + const SizedBox(height: 20), + const Text("تم الدفع بنجاح!", + style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text("العودة"), + ), + ], + ); + } + + Widget _buildErrorUI() { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error, color: Colors.red, size: 80), + const SizedBox(height: 20), + Text( + _status == PaymentStatus.paymentTimeout + ? "انتهى الوقت المحدد للدفع" + : "حدث خطأ ما", + style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + const Text("يرجى المحاولة مرة أخرى.", style: TextStyle(fontSize: 16)), + const SizedBox(height: 20), + ElevatedButton( + onPressed: _createAndPollInvoice, + child: const Text("المحاولة مرة أخرى"), + ), + ], + ); + } +} + +// ويدجت مساعد لعرض خطوات التعليمات بشكل أنيق +class _StepTile extends StatelessWidget { + final int number; + final String text; + const _StepTile({required this.number, required this.text}); + + @override + Widget build(BuildContext context) { + return ListTile( + contentPadding: EdgeInsets.zero, + leading: CircleAvatar( + radius: 12, + backgroundColor: Theme.of(context).primaryColor, + child: Text("$number", + style: const TextStyle( + fontSize: 12, + color: Colors.white, + fontWeight: FontWeight.bold)), + ), + title: Text(text), + ); + } +} diff --git a/lib/controller/profile/captain_profile_controller.dart b/lib/controller/profile/captain_profile_controller.dart index 617c6e8..a25c6c4 100755 --- a/lib/controller/profile/captain_profile_controller.dart +++ b/lib/controller/profile/captain_profile_controller.dart @@ -29,7 +29,7 @@ class CaptainProfileController extends GetxController { await CRUD().post(link: AppLink.updateDriverEmail, payload: payload); if ((res)['status'] == 'success') { - box.write(BoxName.email, emailController.text); + box.write(BoxName.emailDriver, emailController.text); update(); Get.back(); } else { diff --git a/lib/main.dart b/lib/main.dart index 8b28dd6..647c9da 100755 --- a/lib/main.dart +++ b/lib/main.dart @@ -144,14 +144,28 @@ void main() async { DeviceOrientation.portraitDown, ]); runZonedGuarded>(() async { - // ... الكود الحالي الموجود في دالة main ... runApp(const MyApp()); }, (error, stack) { - // أي خطأ غير متوقع في التطبيق سيتم التقاطه هنا CRUD. + // ==== START: ERROR FILTER ==== + String errorString = error.toString(); + + // Print all errors to the local debug console for development print("Caught Dart error: $error"); print(stack); - // أرسل الخطأ إلى السيرفر - CRUD.addError(error.toString(), stack.toString(), 'main'); + + // We will check if the error contains keywords for errors we want to ignore. + // If it's one of them, we will NOT send it to the server. + bool isIgnoredError = errorString.contains('PERMISSION_DENIED') || + errorString.contains('FormatException') || + errorString.contains('Null check operator used on a null value'); + + if (!isIgnoredError) { + // Only send the error to the server if it's not in our ignore list. + CRUD.addError(error.toString(), stack.toString(), 'main'); + } else { + print("Ignoring error and not sending to server: $errorString"); + } + // ==== END: ERROR FILTER ==== }); } diff --git a/lib/views/auth/syria/registration_view.dart b/lib/views/auth/syria/registration_view.dart index 2418509..3df65eb 100644 --- a/lib/views/auth/syria/registration_view.dart +++ b/lib/views/auth/syria/registration_view.dart @@ -284,6 +284,9 @@ class RegistrationView extends StatelessWidget { // STEP 3 Widget _buildDocumentUploadStep(BuildContext ctx, RegistrationController c) { + final String linkUpload = + 'https://syria.intaleq.xyz/intaleq/auth/syria/uploadImage.php'; + return GetBuilder( builder: (ctrl) => SingleChildScrollView( padding: const EdgeInsets.all(16), @@ -296,23 +299,31 @@ class RegistrationView extends StatelessWidget { const SizedBox(height: 20), _buildImagePickerBox( 'Driver License (Front)'.tr, - ctrl.driverLicenseFrontImage, - () => ctrl.pickImage(ImageType.driverLicenseFront), + ctrl.docUrls['driver_license_front'], + // () => ctrl.pickImage(ImageType.driverLicenseFront), + + () async => + await ctrl.choosImage(linkUpload, 'driver_license_front'), ), _buildImagePickerBox( 'Driver License (Back)'.tr, - ctrl.driverLicenseBackImage, - () => ctrl.pickImage(ImageType.driverLicenseBack), + ctrl.docUrls['driver_license_back'], + () async => + await ctrl.choosImage(linkUpload, 'driver_license_back'), + // () => ctrl.pickImage(ImageType.driverLicenseBack), ), _buildImagePickerBox( 'Car Registration (Front)'.tr, - ctrl.carLicenseFrontImage, - () => ctrl.pickImage(ImageType.carLicenseFront), + ctrl.docUrls['car_license_front'], + () async => + await ctrl.choosImage(linkUpload, 'car_license_front'), + // () => ctrl.pickImage(ImageType.carLicenseFront), ), _buildImagePickerBox( 'Car Registration (Back)'.tr, - ctrl.carLicenseBackImage, - () => ctrl.pickImage(ImageType.carLicenseBack), + ctrl.docUrls['car_license_back'], + () async => await ctrl.choosImage(linkUpload, 'car_license_back'), + // () => ctrl.pickImage(ImageType.carLicenseBack), ), ], ), @@ -329,7 +340,7 @@ class RegistrationView extends StatelessWidget { ); } - Widget _buildImagePickerBox(String title, File? img, VoidCallback onTap) { + Widget _buildImagePickerBox(String title, String? img, VoidCallback onTap) { return Card( margin: const EdgeInsets.only(bottom: 16), child: InkWell( @@ -337,8 +348,24 @@ class RegistrationView extends StatelessWidget { child: SizedBox( height: 150, width: double.infinity, - child: img != null - ? Image.file(img, fit: BoxFit.fill) + child: (img != null && img.isNotEmpty) + ? Image.network( + img, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.broken_image, size: 40, color: Colors.red), + const SizedBox(height: 8), + Text('Image not available', + style: TextStyle(color: Colors.red[700])), + ], + ), + ); + }, + ) : Column( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart b/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart index cfe17c5..9ce68fb 100755 --- a/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart +++ b/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart @@ -1,5 +1,7 @@ import 'package:sefer_driver/constant/box_name.dart'; import 'package:sefer_driver/controller/firebase/local_notification.dart'; +import 'package:sefer_driver/controller/functions/network/net_guard.dart'; +import 'package:sefer_driver/controller/functions/sms_egypt_controller.dart'; import 'package:sefer_driver/main.dart'; import 'package:sefer_driver/views/auth/captin/login_captin.dart'; import 'package:sefer_driver/views/home/Captin/driver_map_page.dart'; @@ -14,10 +16,12 @@ import '../../../../../constant/colors.dart'; import '../../../../../constant/links.dart'; import '../../../../../controller/firebase/firbase_messge.dart'; import '../../../../../controller/functions/crud.dart'; +import '../../../../../controller/functions/encrypt_decrypt.dart'; import '../../../../../controller/home/captin/order_request_controller.dart'; import '../../../../../controller/home/navigation/navigation_view.dart'; import '../../../../Rate/ride_calculate_driver.dart'; import '../../../../auth/syria/registration_view.dart'; +import '../../../../widgets/error_snakbar.dart'; GetBuilder leftMainMenuCaptainIcons() { final firebaseMessagesController = @@ -181,10 +185,8 @@ GetBuilder leftMainMenuCaptainIcons() { // child: Builder(builder: (context) { // return IconButton( // onPressed: () async { - // box.remove(BoxName.agreeTerms); - // Get.to(() => const NavigationView()); - - // // box.write(BoxName.statusDriverLocation, 'off'); + // var finger = await storage.read(key: BoxName.fingerPrint); + // // }, // icon: const Icon( // FontAwesome5.grin_tears, diff --git a/lib/views/home/Captin/orderCaptin/order_over_lay.dart b/lib/views/home/Captin/orderCaptin/order_over_lay.dart index 2575a66..b783e71 100755 --- a/lib/views/home/Captin/orderCaptin/order_over_lay.dart +++ b/lib/views/home/Captin/orderCaptin/order_over_lay.dart @@ -6,6 +6,7 @@ import 'package:just_audio/just_audio.dart'; import 'package:sefer_driver/constant/api_key.dart'; import '../../../../constant/box_name.dart'; import '../../../../constant/links.dart'; +import '../../../../controller/firebase/firbase_messge.dart'; import '../../../../controller/firebase/local_notification.dart'; import '../../../../controller/functions/crud.dart'; import '../../../../main.dart'; @@ -218,16 +219,22 @@ class _OrderOverlayState extends State 'status': 'Apply', 'driver_id': box.read(BoxName.driverID), }); - if (AppLink.endPoint != AppLink.seferCairoServer) { - CRUD().post( - link: "${AppLink.endPoint}/ride/rides/updateStausFromSpeed.php", - payload: { - 'id': orderData!.orderId, - 'rideTimeStart': DateTime.now().toString(), - 'status': 'Apply', - 'driver_id': box.read(BoxName.driverID), - }); - } + List bodyToPassenger = [ + _getData(6).toString(), + _getData(8).toString(), + _getData(9).toString(), + ]; + final fmc = Get.isRegistered() + ? Get.find() + : Get.put(FirebaseMessagesController()); + + fmc.sendNotificationToDriverMAP( + "Accepted Ride", + 'your ride is Accepted'.tr, + _getData(9).toString(), + bodyToPassenger, + 'start.wav', + ); final payload = { // بيانات أساسية 'driver_id': driverId, diff --git a/lib/views/home/Captin/orderCaptin/order_request_page.dart b/lib/views/home/Captin/orderCaptin/order_request_page.dart index ce5c27d..5a944d3 100755 --- a/lib/views/home/Captin/orderCaptin/order_request_page.dart +++ b/lib/views/home/Captin/orderCaptin/order_request_page.dart @@ -280,7 +280,7 @@ class _OrderRequestPageState extends State { : Get.put(FirebaseMessagesController()); fmc.sendNotificationToDriverMAP( - "Accepted Ride".tr, + "Accepted Ride", 'your ride is Accepted'.tr, controller.myList[9].toString(), bodyToPassenger, diff --git a/lib/views/home/my_wallet/card_wallet_widget.dart b/lib/views/home/my_wallet/card_wallet_widget.dart index 627bb6c..1d2dbe5 100755 --- a/lib/views/home/my_wallet/card_wallet_widget.dart +++ b/lib/views/home/my_wallet/card_wallet_widget.dart @@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; +import 'package:sefer_driver/views/home/my_wallet/pay_out_screen.dart'; import '../../../constant/box_name.dart'; import '../../../constant/colors.dart'; @@ -164,7 +165,7 @@ class CardSeferWalletDriver extends StatelessWidget { void _showCashOutDialog( BuildContext context, CaptainWalletController captainWalletController) { - double minAmount = 20.0; // الحد الأدنى للسحب + double minAmount = 20000.0; // الحد الأدنى للسحب if (double.parse(captainWalletController.totalAmountVisa) >= minAmount) { Get.defaultDialog( barrierDismissible: false, @@ -208,16 +209,29 @@ class CardSeferWalletDriver extends StatelessWidget { confirm: MyElevatedButton( title: 'تأكيد'.tr, onPressed: () async { + box.write( + BoxName.phoneWallet, captainWalletController.phoneWallet); + box.write(BoxName.walletType, + Get.find().dropdownValue.toString()); if (captainWalletController.formKey.currentState!.validate()) { Get.back(); - String amountAfterFee = - (double.parse(captainWalletController.totalAmountVisa) - 5) - .toStringAsFixed(0); - await Get.put(PaymobPayout()).payToWalletDriverAll( - amountAfterFee, - Get.find().dropdownValue.toString(), - captainWalletController.phoneWallet.text.toString(), - ); + Get.to(() => PayoutScreen( + amountToWithdraw: + double.parse(captainWalletController.totalAmountVisa), + payoutPhoneNumber: + captainWalletController.phoneWallet.text.toString(), + walletType: Get.find() + .dropdownValue + .toString(), + )); + // String amountAfterFee = + // (double.parse(captainWalletController.totalAmountVisa) - 5) + // .toStringAsFixed(0); + // await Get.put(PaymobPayout()).payToWalletDriverAll( + // amountAfterFee, + // Get.find().dropdownValue.toString(), + // captainWalletController.phoneWallet.text.toString(), + // ); } }, kolor: AppColor.greenColor, @@ -274,7 +288,7 @@ class MyDropDownSyria extends StatelessWidget { onChanged: (String? newValue) { controller.changeValue(newValue); }, - items: ['syriatel', 'mtn'] + items: ['Syriatel', 'Cash Mobile', 'Sham Cash'] .map>((String value) { return DropdownMenuItem( value: value, @@ -288,7 +302,7 @@ class MyDropDownSyria extends StatelessWidget { // هذا المتحكم ضروري لعمل القائمة المنسدلة class SyrianPayoutController extends GetxController { - String dropdownValue = 'syriatel'; + String dropdownValue = 'Syriatel'; void changeValue(String? newValue) { if (newValue != null) { diff --git a/lib/views/home/my_wallet/pay_out_screen.dart b/lib/views/home/my_wallet/pay_out_screen.dart new file mode 100644 index 0000000..e835523 --- /dev/null +++ b/lib/views/home/my_wallet/pay_out_screen.dart @@ -0,0 +1,196 @@ +import 'package:flutter/material.dart'; +import 'package:local_auth/local_auth.dart'; +import 'package:sefer_driver/constant/box_name.dart'; +import 'package:sefer_driver/main.dart'; + +import '../../../controller/payment/smsPaymnet/pay_out_syria_controller.dart'; + +class PayoutScreen extends StatefulWidget { + // استقبال كل البيانات المطلوبة جاهزة + final double amountToWithdraw; + final String payoutPhoneNumber; + final String walletType; + + const PayoutScreen({ + super.key, + required this.amountToWithdraw, + required this.payoutPhoneNumber, + required this.walletType, + }); + + @override + _PayoutScreenState createState() => _PayoutScreenState(); +} + +class _PayoutScreenState extends State { + final _payoutService = PayoutService(); + final _localAuth = LocalAuthentication(); + bool _isLoading = false; + + Future _handlePayoutRequest() async { + try { + // 1. طلب المصادقة البيومترية + bool didAuthenticate = await _localAuth.authenticate( + localizedReason: 'استخدم بصمة الإصبع لتأكيد عملية السحب', + options: const AuthenticationOptions( + biometricOnly: true, + sensitiveTransaction: true, + ), + ); + + if (didAuthenticate && mounted) { + setState(() => _isLoading = true); + + // 2. إرسال الطلب إلى السيرفر بالبيانات الجاهزة + final result = await _payoutService.requestPayout( + driverId: + box.read(BoxName.driverID).toString(), // استبدله بـ box.read + amount: widget.amountToWithdraw, + payoutPhoneNumber: widget.payoutPhoneNumber, + walletType: widget.walletType, + ); + + setState(() => _isLoading = false); + + if (result != null && result.contains("successfully")) { + // 3. عرض رسالة النجاح النهائية + _showSuccessDialog(); + } else { + _showErrorDialog(result ?? "حدث خطأ غير معروف."); + } + } + } catch (e) { + setState(() => _isLoading = false); + _showErrorDialog("جهازك لا يدعم المصادقة البيومترية أو لم يتم إعدادها."); + debugPrint("Biometric error: $e"); + } + } + + @override + Widget build(BuildContext context) { + // حساب المبلغ الإجمالي المخصوم + final totalDeducted = widget.amountToWithdraw + PayoutService.payoutFee; + + return Scaffold( + appBar: AppBar(title: const Text("تأكيد سحب الأموال")), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Icon(Icons.wallet, size: 64, color: Colors.blue), + const SizedBox(height: 16), + Text( + "تأكيد تفاصيل عملية السحب", + style: Theme.of(context).textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + _buildSummaryCard(totalDeducted), + const SizedBox(height: 32), + _isLoading + ? const Center(child: CircularProgressIndicator()) + : ElevatedButton.icon( + onPressed: _handlePayoutRequest, + icon: const Icon(Icons.fingerprint), + label: const Text("تأكيد السحب بالبصمة"), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + textStyle: const TextStyle( + fontSize: 18, fontWeight: FontWeight.bold), + ), + ), + ], + ), + ), + ); + } + + Widget _buildSummaryCard(double totalDeducted) { + return Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + _summaryRow("المبلغ المسحوب:", + "${widget.amountToWithdraw.toStringAsFixed(2)} ل.س"), + const Divider(), + _summaryRow("عمولة السحب:", + "${PayoutService.payoutFee.toStringAsFixed(2)} ل.س"), + const Divider(thickness: 1.5), + _summaryRow( + "الإجمالي المخصوم من رصيدك:", + "${totalDeducted.toStringAsFixed(2)} ل.س", + isTotal: true, + ), + const SizedBox(height: 16), + _summaryRow("سيتم التحويل إلى هاتف:", widget.payoutPhoneNumber), + _summaryRow("عبر محفظة:", widget.walletType), + ], + ), + ), + ); + } + + Widget _summaryRow(String title, String value, {bool isTotal = false}) { + final titleStyle = TextStyle( + fontSize: 16, + color: isTotal ? Theme.of(context).primaryColor : Colors.black87, + fontWeight: isTotal ? FontWeight.bold : FontWeight.normal, + ); + final valueStyle = titleStyle.copyWith( + fontWeight: FontWeight.bold, + ); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, style: titleStyle), + Text(value, style: valueStyle), + ], + ), + ); + } + + void _showErrorDialog(String message) { + if (!mounted) return; + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('حدث خطأ'), + content: Text(message), + actions: [ + TextButton( + child: const Text('موافق'), + onPressed: () => Navigator.of(ctx).pop()) + ], + ), + ); + } + + void _showSuccessDialog() { + if (!mounted) return; + showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => AlertDialog( + title: const Text('تم إرسال طلبك بنجاح'), + content: Text( + "سيتم تحويل المال إلى المحفظة التي أوردتها (${widget.walletType})، إلى الرقم ${widget.payoutPhoneNumber}، خلال مدة قصيرة. يرجى الانتظار، ستصلك رسالة تأكيد من محفظتك حال وصولها. شكراً لك."), + actions: [ + TextButton( + child: const Text('موافق'), + onPressed: () { + Navigator.of(ctx).pop(); + Navigator.of(context).pop(); + }, + ), + ], + ), + ); + } +} diff --git a/lib/views/home/my_wallet/points_captain.dart b/lib/views/home/my_wallet/points_captain.dart index 8903db8..c66ec94 100755 --- a/lib/views/home/my_wallet/points_captain.dart +++ b/lib/views/home/my_wallet/points_captain.dart @@ -7,12 +7,14 @@ import 'package:sefer_driver/constant/colors.dart'; import 'package:sefer_driver/constant/style.dart'; import 'package:sefer_driver/controller/home/payment/captain_wallet_controller.dart'; import 'package:sefer_driver/controller/payment/payment_controller.dart'; +import 'package:sefer_driver/controller/payment/smsPaymnet/payment_services.dart'; import 'package:webview_flutter/webview_flutter.dart'; import '../../../constant/box_name.dart'; import '../../../constant/links.dart'; import '../../../controller/functions/crud.dart'; import '../../../main.dart'; +import '../../../print.dart'; import '../../widgets/elevated_btn.dart'; import '../../widgets/my_textField.dart'; import 'ecash.dart'; @@ -40,21 +42,29 @@ class PointsCaptain extends StatelessWidget { title: 'Which method you will pay'.tr, titleStyle: AppStyle.title, content: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '${'you can buy '.tr}$countPoint ${'L.S'.tr}${'by '.tr}${'$pricePoint'.tr}', style: AppStyle.title, ), - MyElevatedButton( - title: 'Pay with Credit Card'.tr, - onPressed: () async { - Get.back(); - payWithEcashDriver(context, pricePoint.toString()); - // var d = jsonDecode(res); - }, //51524 - ), // Add some spacing between buttons + GestureDetector( + onTap: () async { + Get.back(); + payWithEcashDriver(context, pricePoint.toString()); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Pay with Debit Card'.tr), + const SizedBox(width: 10), + Icon(Icons.credit_card_sharp, + color: AppColor.blueColor, size: 70), + ], + )), GestureDetector( onTap: () async { Get.back(); @@ -84,44 +94,122 @@ class PointsCaptain extends StatelessWidget { } })); }, - child: Image.asset( - 'assets/images/mtn.png', - width: 70, - height: 70, - fit: BoxFit.fill, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Pay by MTN Wallet'.tr), + const SizedBox(width: 10), + Image.asset( + 'assets/images/cashMTN.png', + width: 70, + height: 70, + fit: BoxFit.fill, + ), + ], + )), + GestureDetector( + onTap: () async { + Get.back(); + Get.defaultDialog( + barrierDismissible: false, + title: 'Insert Wallet phone number'.tr, + content: Form( + key: paymentController.formKey, + child: MyTextForm( + controller: + paymentController.walletphoneController, + label: 'Insert Wallet phone number'.tr, + hint: '963991234567', + type: TextInputType.phone)), + confirm: MyElevatedButton( + title: 'OK'.tr, + onPressed: () async { + Get.back(); + if (paymentController.formKey.currentState! + .validate()) { + box.write( + BoxName.phoneWallet, + paymentController + .walletphoneController.text); + await payWithSyriaTelWallet( + context, pricePoint.toString(), 'SYP'); + } + })); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Pay by Syriatel Wallet'.tr), + const SizedBox(width: 10), + Image.asset( + 'assets/images/syriatel.jpeg', + width: 70, + height: 70, + fit: BoxFit.fill, + ), + ], + )), + GestureDetector( + onTap: () async { + Get.back(); + Get.defaultDialog( + barrierDismissible: false, + title: 'Insert Wallet phone number'.tr, + content: Form( + key: paymentController.formKey, + child: MyTextForm( + controller: + paymentController.walletphoneController, + label: 'Insert Wallet phone number'.tr, + hint: '963941234567', + type: TextInputType.phone)), + confirm: MyElevatedButton( + title: 'OK'.tr, + onPressed: () async { + Get.back(); + if (paymentController.formKey.currentState! + .validate()) { + box.write( + BoxName.phoneWallet, + paymentController + .walletphoneController.text); + // await payWithSyriaTelWallet( + // context, pricePoint.toString(), 'SYP'); + bool isAuthSupported = + await LocalAuthentication() + .isDeviceSupported(); + if (isAuthSupported) { + bool didAuthenticate = + await LocalAuthentication() + .authenticate( + localizedReason: + 'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع', + ); + if (!didAuthenticate) { + if (Get.isDialogOpen ?? false) Get.back(); + print( + "❌ User did not authenticate with biometrics"); + return; + } + } + Get.to(() => PaymentScreenSmsProvider( + amount: pricePoint)); + } + })); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Pay by Sham Cash'.tr), + const SizedBox(width: 10), + Image.asset( + 'assets/images/shamCash.png', + width: 70, + height: 70, + fit: BoxFit.fill, + ), + ], )), - // MyElevatedButton( - // kolor: AppColor.redColor, - // title: 'Pay with Wallet'.tr, - // onPressed: () async { - // Get.back(); - // Get.defaultDialog( - // barrierDismissible: false, - // title: 'Insert Wallet phone number'.tr, - // content: Form( - // key: paymentController.formKey, - // child: MyTextForm( - // controller: - // paymentController.walletphoneController, - // label: 'Insert Wallet phone number'.tr, - // hint: '963941234567', - // type: TextInputType.phone)), - // confirm: MyElevatedButton( - // title: 'OK'.tr, - // onPressed: () async { - // Get.back(); - // if (paymentController.formKey.currentState! - // .validate()) { - // box.write( - // BoxName.phoneWallet, - // paymentController - // .walletphoneController.text); - // await payWithMTNWallet( - // context, pricePoint.toString(), 'SYP'); - // } - // })); - // }, - // ), ], )); }, @@ -485,7 +573,7 @@ Future payWithMTNWallet( barrierDismissible: false); try { - String phone = box.read(BoxName.phoneWallet) ?? '963992952235'; + String phone = box.read(BoxName.phoneWallet); String driverID = box.read(BoxName.driverID).toString(); String formattedAmount = double.parse(amount).toStringAsFixed(0); @@ -507,12 +595,13 @@ Future payWithMTNWallet( } // 1️⃣ استدعاء mtn_start_payment.php (الملف الجديد) - var responseData = await CRUD().postWallet( + var responseData = await CRUD().postWalletMtn( link: AppLink.payWithMTNStart, payload: { "amount": formattedAmount, "passengerId": driverID, "phone": phone, + "lang": box.read(BoxName.lang) ?? 'ar', }, ); @@ -540,7 +629,7 @@ Future payWithMTNWallet( } if (startRes['status'] != 'success') { - String errorMsg = startRes['message']?.toString() ?? + final errorMsg = startRes['message']['Error']?.toString().tr ?? "فشل بدء عملية الدفع. حاول مرة أخرى."; throw Exception(errorMsg); } @@ -555,7 +644,7 @@ Future payWithMTNWallet( print( "📄 invoiceNumber: $invoiceNumber, 🔢 operationNumber: $operationNumber, 🧭 guid: $guid"); - if (Get.isDialogOpen ?? false) + if (Get.isDialogOpen == true) Get.back(); // إغلاق مؤشر التحميل قبل عرض حوار OTP // 2️⃣ عرض واجهة إدخال OTP @@ -609,7 +698,8 @@ Future payWithMTNWallet( if (Get.isDialogOpen ?? false) Get.back(); print("✅ استجابة mtn_confirm.php:"); - print(confirmRes); + // print(confirmRes); + Log.print('confirmRes: ${confirmRes}'); if (confirmRes != null && confirmRes['status'] == 'success') { Get.defaultDialog( @@ -635,3 +725,158 @@ Future payWithMTNWallet( ); } } + +Future payWithSyriaTelWallet( + BuildContext context, String amount, String currency) async { + // Show a loading indicator for better user experience + Get.dialog(const Center(child: CircularProgressIndicator()), + barrierDismissible: false); + + try { + String phone = box.read(BoxName.phoneWallet); + String driverID = box.read(BoxName.driverID).toString(); + String formattedAmount = double.parse(amount).toStringAsFixed(0); + + // --- CHANGE 1: Updated log messages for clarity --- + print("🚀 Starting Syriatel payment process"); + print( + "📦 Payload: driverID: $driverID, amount: $formattedAmount, phone: $phone"); + + // Optional: Biometric authentication + bool isAuthSupported = await LocalAuthentication().isDeviceSupported(); + if (isAuthSupported) { + bool didAuthenticate = await LocalAuthentication().authenticate( + localizedReason: 'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع', + ); + if (!didAuthenticate) { + if (Get.isDialogOpen ?? false) Get.back(); + print("❌ User did not authenticate with biometrics"); + return; + } + } + + // --- CHANGE 2: Updated API link and payload for starting payment --- + // Make sure you have defined `payWithSyriatelStart` in your AppLink class + var responseData = await CRUD().postWalletMtn( + link: AppLink.payWithSyriatelStart, // Use the new Syriatel start link + payload: { + "amount": formattedAmount, + "driverId": driverID, // Key changed from 'passengerId' to 'driverId' + "phone": phone, + "lang": box.read(BoxName.lang) ?? 'ar', + }, + ); + + print("✅ Server response (start_payment.php):"); + Log.print('responseData: ${responseData}'); + + // Robustly parse the server's JSON response + Map startRes; + if (responseData is Map) { + startRes = responseData; + } else if (responseData is String) { + try { + startRes = json.decode(responseData); + } catch (e) { + throw Exception( + "Failed to parse server response. Response: $responseData"); + } + } else { + throw Exception("Received an unexpected data type from the server."); + } + + if (startRes['status'] != 'success') { + String errorMsg = startRes['message']?.toString() ?? + "Failed to start the payment process. Please try again."; + throw Exception(errorMsg); + } + + // --- CHANGE 3: Extract `transactionID` from the response --- + // The response structure is now simpler. We only need the transaction ID. + final messageData = startRes["message"]; + final transactionID = messageData["transactionID"].toString(); + + print("📄 TransactionID: $transactionID"); + + if (Get.isDialogOpen == true) Get.back(); // Close loading indicator + + // Show the OTP input dialog + String? otp = await showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + String input = ""; + return AlertDialog( + title: const Text("أدخل كود التحقق"), + content: TextField( + keyboardType: TextInputType.number, + decoration: const InputDecoration(hintText: "كود OTP"), + onChanged: (val) => input = val, + ), + actions: [ + TextButton( + child: const Text("تأكيد"), + onPressed: () => Navigator.of(context).pop(input), + ), + TextButton( + child: const Text("إلغاء"), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ); + }, + ); + + if (otp == null || otp.isEmpty) { + print("❌ OTP was not entered."); + return; + } + print("🔐 OTP entered: $otp"); + + Get.dialog(const Center(child: CircularProgressIndicator()), + barrierDismissible: false); + + // --- CHANGE 4: Updated API link and payload for confirming payment --- + // Make sure you have defined `payWithSyriatelConfirm` in your AppLink class + var confirmRes = await CRUD().postWalletMtn( + // Changed from postWalletMtn if they are different + link: AppLink.payWithSyriatelConfirm, // Use the new Syriatel confirm link + payload: { + "transactionID": transactionID, // Use the transaction ID + "otp": otp, + // The other parameters (phone, guid, etc.) are no longer needed + }, + ); + + if (Get.isDialogOpen ?? false) Get.back(); + + print("✅ Response from confirm_payment.php:"); + Log.print('confirmRes: ${confirmRes}'); + + if (confirmRes != null && confirmRes['status'] == 'success') { + Get.defaultDialog( + title: "✅ نجاح", + content: const Text("تمت عملية الدفع وإضافة الرصيد إلى محفظتك."), + ); + } else { + // --- CHANGE 5: Simplified error message extraction --- + // The new PHP script sends the error directly in the 'message' field. + String errorMsg = + confirmRes?['message']?.toString() ?? "فشل في تأكيد الدفع"; + Get.defaultDialog( + title: "❌ فشل", + content: Text(errorMsg.tr), + ); + } + } catch (e, s) { + // --- CHANGE 6: Updated general error log message --- + print("🔥 Error during Syriatel Wallet payment:"); + print(e); + print(s); + if (Get.isDialogOpen ?? false) Get.back(); + Get.defaultDialog( + title: 'حدث خطأ', + content: Text(e.toString().replaceFirst("Exception: ", "")), + ); + } +}