diff --git a/android/app/build.gradle b/android/app/build.gradle index 519e006..f98c0dc 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -47,8 +47,8 @@ android { // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = 29 targetSdk = 36 - versionCode = 11 - versionName = '1.0.11' + versionCode = 13 + versionName = '1.0.13' multiDexEnabled = true ndk { abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b17173d..d7cc69f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -43,15 +43,15 @@ - - + + + + - - - - + + diff --git a/android/build.gradle b/android/build.gradle index 395d584..f4c4022 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -30,7 +30,7 @@ buildscript { // classpath 'com.android.tools.build:gradle:7.3.1' classpath 'com.google.gms:google-services:4.3.15' // END: FlutterFire Configuration - classpath 'com.android.tools.build:gradle:8.11.0' + classpath 'com.android.tools.build:gradle:8.11.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html index 128d91d..e1e2989 100644 --- a/android/build/reports/problems/problems-report.html +++ b/android/build/reports/problems/problems-report.html @@ -650,7 +650,7 @@ code + .copy-button { diff --git a/android/settings.gradle b/android/settings.gradle index 4fc9848..9992d0a 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -18,7 +18,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version '8.11.0' apply false + id "com.android.application" version '8.11.1' apply false // START: FlutterFire Configuration id "com.google.gms.google-services" version "4.3.10" apply false // END: FlutterFire Configuration diff --git a/assets/images/mtn.png b/assets/images/mtn.png new file mode 100644 index 0000000..6aa6618 Binary files /dev/null and b/assets/images/mtn.png differ diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 207588a..20dc7d2 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -11,6 +11,8 @@ PODS: - PromisesObjC (~> 2.4) - audio_session (0.0.1): - Flutter + - connectivity_plus (0.0.1): + - Flutter - device_info_plus (0.0.1): - Flutter - Firebase/Auth (11.15.0): @@ -251,6 +253,7 @@ PODS: DEPENDENCIES: - audio_session (from `.symlinks/plugins/audio_session/ios`) + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) @@ -323,6 +326,8 @@ SPEC REPOS: EXTERNAL SOURCES: audio_session: :path: ".symlinks/plugins/audio_session/ios" + connectivity_plus: + :path: ".symlinks/plugins/connectivity_plus/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" firebase_auth: @@ -396,6 +401,7 @@ SPEC CHECKSUMS: AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0 + connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e firebase_auth: 50af8366c87bb88c80ebeae62eb60189c7246b9b diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index e0e8b89..976b1f9 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -322,14 +322,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -343,14 +339,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index e346993..f042e64 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -1,100 +1,117 @@ - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Intaleq - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - Intaleq - CFBundlePackageType - APPL - CFBundleShortVersionString - 6 - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLSchemes - - com.googleusercontent.apps.1086900987150-9jv4oa8l3t23d54lrf27c1d22tbt9i6d - - - - - CFBundleVersion - 1.0.6 - FirebaseAppDelegateProxyEnabled - NO - GMSApiKey - YOUR_API_KEY - LSApplicationQueriesSchemes - - googlechromes - comgooglemaps - - LSRequiresIPhoneOS - - NSCameraUsageDescription - This app requires access to your camera in order to scan QR codes and capture images + + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + + com.intaleq.app + CFBundleURLSchemes + + intaleq + + + + FlutterDeepLinkingEnabled + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Intaleq + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Intaleq + CFBundlePackageType + APPL + CFBundleShortVersionString + 7 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + + com.googleusercontent.apps.1086900987150-9jv4oa8l3t23d54lrf27c1d22tbt9i6d + + + + + CFBundleVersion + 1.0.7 + FirebaseAppDelegateProxyEnabled + NO + GMSApiKey + YOUR_API_KEY + LSApplicationQueriesSchemes + + googlechromes + comgooglemaps + + LSRequiresIPhoneOS + + NSCameraUsageDescription + This app requires access to your camera in order to scan QR codes and capture images for uploading and access to connect to a call. - NSContactsUsageDescription - This app requires contacts access to function properly. - NSFaceIDUsageDescription - Use Face ID to securely authenticate payment accounts. - NSLocationAlwaysAndWhenInUseUsageDescription - This app needs access to your location to provide you with the best ride experience. + NSContactsUsageDescription + This app requires contacts access to function properly. + NSFaceIDUsageDescription + Use Face ID to securely authenticate payment accounts. + NSLocationAlwaysAndWhenInUseUsageDescription + This app needs access to your location to provide you with the best ride experience. Your location data will be used to find the nearest available cars and connect you with the closest captain for efficient and convenient rides. - NSLocationAlwaysUsageDescription - This app needs access to location. - NSLocationWhenInUseUsageDescription - This app needs access to your location to provide you with the best ride experience. + NSLocationAlwaysUsageDescription + This app needs access to location. + NSLocationWhenInUseUsageDescription + This app needs access to your location to provide you with the best ride experience. Your location data will be used to find the nearest available cars and connect you with the closest captain for efficient and convenient rides. - NSMicrophoneUsageDescription - This app requires access to your microphone to record audio, allowing you to add + NSMicrophoneUsageDescription + This app requires access to your microphone to record audio, allowing you to add voice recordings to your photos and videos and access to connect to a call. - NSPhotoLibraryUsageDescription - This app requires access to the photo library to upload pictures. - UIApplicationSupportsIndirectInputEvents - - UIBackgroundModes - - fetch - location - remote-notification - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - + NSPhotoLibraryUsageDescription + This app requires access to the photo library to upload pictures. + UIApplicationSupportsIndirectInputEvents + + UIBackgroundModes + + fetch + location + remote-notification + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + \ No newline at end of file diff --git a/lib/controller/auth/login_controller.dart b/lib/controller/auth/login_controller.dart index bcc9157..84b3a8f 100644 --- a/lib/controller/auth/login_controller.dart +++ b/lib/controller/auth/login_controller.dart @@ -242,6 +242,7 @@ class LoginController extends GetxController { if ((jsonDecode(token)['message']['token'].toString()) != box.read(BoxName.tokenFCM)) { await Get.defaultDialog( + barrierDismissible: false, title: 'Device Change Detected'.tr, middleText: 'Please verify your identity'.tr, textConfirm: 'Verify'.tr, diff --git a/lib/controller/functions/crud.dart b/lib/controller/functions/crud.dart index 2c736cb..2db9301 100644 --- a/lib/controller/functions/crud.dart +++ b/lib/controller/functions/crud.dart @@ -9,238 +9,326 @@ import 'package:Intaleq/env/env.dart'; import '../../constant/api_key.dart'; -import '../../print.dart'; import '../../views/widgets/elevated_btn.dart'; import '../../views/widgets/error_snakbar.dart'; -import 'add_error.dart'; import 'encrypt_decrypt.dart'; import 'upload_image.dart'; +import 'dart:io'; + +import 'package:jwt_decoder/jwt_decoder.dart'; + +import 'network/connection_check.dart'; +import 'network/net_guard.dart'; class CRUD { + final NetGuard _netGuard = NetGuard(); + + /// 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 addError( + String error, String details, String where) async { + try { + final currentErrorSignature = '$where-$error'; + final now = DateTime.now(); + + if (currentErrorSignature == _lastErrorSignature && + now.difference(_lastErrorTimestamp) < _errorLogDebounceDuration) { + print("Debounced a duplicate error: $error"); + 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); + + // 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) { + print("CRITICAL: Failed to log error to server: $e"); + } + } + + /// Centralized private method to handle all API requests. + /// Includes retry logic, network checking, and standardized error handling. + Future _makeRequest({ + required String link, + Map? payload, + required Map headers, + }) async { + try { + var response = await HttpRetry.sendWithRetry( + () { + var url = Uri.parse(link); + return http.post( + url, + body: payload, + headers: headers, + ); + }, + maxRetries: 3, + timeout: const Duration(seconds: 15), + ); + + if (response.statusCode == 200) { + try { + var jsonData = jsonDecode(response.body); + if (jsonData['status'] == 'success') { + return jsonData; + } else { + // Log API logical errors (e.g., "Customer not found") + if (response.body == 'failure') { + return 'failure'; + } else { + addError( + 'API Logic Error: ${jsonData['status']}', + 'Response: ${response.body}', + 'CRUD._makeRequest - $link', + ); + } + + return jsonData['status']; + } + } catch (e, stackTrace) { + addError( + 'JSON Decode Error: $e', + 'Response Body: ${response.body}\nStack Trace: $stackTrace', + 'CRUD._makeRequest - $link', + ); + return 'failure'; + } + } else if (response.statusCode == 401) { + var jsonData = jsonDecode(response.body); + if (jsonData['error'] == 'Token expired') { + return 'token_expired'; + } else { + addError( + 'Unauthorized Error: ${jsonData['error']}', + 'Status Code: 401', + 'CRUD._makeRequest - $link', + ); + return 'failure'; + } + } else { + addError( + 'HTTP Error', + 'Status Code: ${response.statusCode}\nResponse Body: ${response.body}', + 'CRUD._makeRequest - $link', + ); + return 'failure'; + } + } on SocketException { + _netGuard.notifyOnce((title, msg) { + mySnackeBarError(msg); + }); + return 'no_internet'; + } catch (e, stackTrace) { + addError( + 'HTTP Request Exception: $e', + 'Stack Trace: $stackTrace', + 'CRUD._makeRequest - $link', + ); + return 'failure'; + } + } + + /// Performs a standard authenticated POST request. + /// Automatically handles token renewal. + Future post({ + required String link, + Map? payload, + }) async { + String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]; + if (JwtDecoder.isExpired(token)) { + await Get.put(LoginController()).getJWT(); + token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]; + } + + final headers = { + "Content-Type": "application/x-www-form-urlencoded", + 'Authorization': 'Bearer $token' + }; + + return await _makeRequest( + link: link, + payload: payload, + headers: headers, + ); + } + + /// Performs a standard authenticated GET request (using POST method as per original code). + /// Automatically handles token renewal. Future get({ required String link, Map? payload, }) async { - // print(r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]); - 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]}' - }, - ); - // print('req: ${response.request}'); - // Log.print('response: ${response.body}'); - // Log.print('payload: ${payload}'); - if (response.statusCode == 200) { - var jsonData = jsonDecode(response.body); - if (jsonData['status'] == 'success') { - return response.body; - } - - return jsonData['status']; - } else if (response.statusCode == 401) { - // Specifically handle 401 Unauthorized - var jsonData = jsonDecode(response.body); - - if (jsonData['error'] == 'Token expired') { - // Show snackbar prompting to re-login - await Get.put(LoginController()).getJWT(); - mySnackbarSuccess('please order now'.tr); - return 'token_expired'; // Return a specific value for token expiration - } else { - // Other 401 errors - addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401'); - return 'failure'; - } - } else { - addError('Non-200 response code: ${response.statusCode}', - 'crud().post - Other'); - return 'failure'; + String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]; + if (JwtDecoder.isExpired(token)) { + await Get.put(LoginController()).getJWT(); + token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]; } + + final headers = { + "Content-Type": "application/x-www-form-urlencoded", + 'Authorization': 'Bearer $token' + }; + + var result = await _makeRequest( + link: link, + payload: payload, + headers: headers, + ); + + // The original 'get' method returned the raw body on success, maintaining that behavior. + if (result is Map && result['status'] == 'success') { + return jsonEncode(result); + } + return result; } + /// Performs an authenticated POST request to wallet endpoints. + Future postWallet({ + required String link, + Map? 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(), + }; + + return await _makeRequest( + link: link, + payload: payload, + headers: headers, + ); + } + + /// Performs an authenticated GET request to wallet endpoints (using POST). Future getWallet({ required String link, Map? payload, }) async { - var s = await LoginController().getJwtWallet(); + var jwt = await LoginController().getJwtWallet(); final hmac = box.read(BoxName.hmac); - // Log.print('hmac: ${hmac}'); - var url = Uri.parse( - link, + + final headers = { + "Content-Type": "application/x-www-form-urlencoded", + 'Authorization': 'Bearer $jwt', + 'X-HMAC-Auth': hmac.toString(), + }; + + var result = await _makeRequest( + link: link, + payload: payload, + headers: headers, ); - var response = await http.post( - url, - body: payload, - headers: { - "Content-Type": "application/x-www-form-urlencoded", - 'Authorization': 'Bearer $s', - 'X-HMAC-Auth': hmac.toString(), - }, - ); - // print('req: ${response.request}'); - // Log.print('response: ${response.body}'); - // Log.print('payload: ${payload}'); - if (response.statusCode == 200) { - var jsonData = jsonDecode(response.body); - Log.print('jsonData: $jsonData'); - if (jsonData['status'] == 'success') { - return response.body; - } - return jsonData['status']; - } else if (response.statusCode == 401) { - // Specifically handle 401 Unauthorized - var jsonData = jsonDecode(response.body); - - if (jsonData['error'] == 'Token expired') { - // Show snackbar prompting to re-login - await Get.put(LoginController()).getJwtWallet(); - - return 'token_expired'; // Return a specific value for token expiration - } else { - // Other 401 errors - addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401'); - return 'failure'; - } - } else { - addError('Non-200 response code: ${response.statusCode}', - 'crud().post - Other'); - return 'failure'; + if (result is Map && result['status'] == 'success') { + return jsonEncode(result); } + return result; } - Future post( + // ======================================================================= + // All other specialized methods remain below. + // They are kept separate because they interact with external third-party APIs + // and have unique authentication, body structures, or error handling logic + // that doesn't fit the standardized `_makeRequest` helper. + // ======================================================================= + + Future postWalletMtn( {required String link, Map? payload}) async { - var url = Uri.parse(link); + // This method has a very custom response-wrapping logic, so it's kept separate. + final s = await LoginController().getJwtWallet(); + final hmac = box.read(BoxName.hmac); + final url = Uri.parse(link); + try { - var response = await http.post( + final 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]}' + "Authorization": "Bearer $s", + "X-HMAC-Auth": hmac.toString(), }, ); - // Log.print('req: ${response.request}'); - // Log.print('response: ${response.body}'); - // Log.print('payload: ${payload}'); + + print('req: ${response.request}'); + print('status: ${response.statusCode}'); + print('body: ${response.body}'); + print('payload: $payload'); + + Map wrap(String status, {Object? message, int? code}) { + return { + 'status': status, + 'message': message, + 'code': code ?? response.statusCode, + }; + } + if (response.statusCode == 200) { try { - var jsonData = jsonDecode(response.body); - if (jsonData['status'] == 'success') { - return jsonData; - } else { - return jsonData['status']; - } + return jsonDecode(response.body); } catch (e) { - // addError(e.toString(), 'crud().post - JSON decoding'); - return 'failure'; + return wrap('failure', + message: 'JSON decode error', code: response.statusCode); } } else if (response.statusCode == 401) { - // Specifically handle 401 Unauthorized - var jsonData = jsonDecode(response.body); - - if (jsonData['error'] == 'Token expired') { - // Show snackbar prompting to re-login - await Get.put(LoginController()).getJWT(); - // MyDialog().getDialog( - // 'Session expired. Please log in again.'.tr, - // '', - // () { - // Get.put(LoginController()).loginUsingCredentials( - // box.read(BoxName.passengerID), box.read(BoxName.email)); - // Get.back(); - // }, - // ); - - return 'token_expired'; // Return a specific value for token expiration - } else { - // Other 401 errors - // addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401'); - return 'failure'; - } - } else { - // addError('Non-200 response code: ${response.statusCode}', - // 'crud().post - Other'); - return 'failure'; - } - } catch (e) { - // addError('HTTP request error: $e', 'crud().post - HTTP'); - return 'failure'; - } - } - - Future postWallet( - {required String link, Map? payload}) async { - var s = await LoginController().getJwtWallet(); - - final hmac = box.read(BoxName.hmac); - - var url = Uri.parse(link); - try { - var response = await http.post( - url, - body: payload, - headers: { - "Content-Type": "application/x-www-form-urlencoded", - 'Authorization': 'Bearer $s', - 'X-HMAC-Auth': hmac.toString(), - }, - ); - // print('req: ${response.request}'); - // Log.print('response: ${response.body}'); - // Log.print('payload: ${payload}'); - if (response.statusCode == 200) { try { - var jsonData = jsonDecode(response.body); - if (jsonData['status'] == 'success') { - return jsonData; - } else { - return jsonData['status']; + 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 + }; } - } catch (e) { - addError(e.toString(), 'crud().post - JSON decoding'); - return 'failure'; - } - } else if (response.statusCode == 401) { - // Specifically handle 401 Unauthorized - var jsonData = jsonDecode(response.body); - - if (jsonData['error'] == 'Token expired') { - // Show snackbar prompting to re-login - await Get.put(LoginController()).getJWT(); - // MyDialog().getDialog( - // 'Session expired. Please log in again.'.tr, - // '', - // () { - // Get.put(LoginController()).loginUsingCredentials( - // box.read(BoxName.passengerID), box.read(BoxName.email)); - // Get.back(); - // }, - // ); - - return 'token_expired'; // Return a specific value for token expiration - } else { - // Other 401 errors - // addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401'); - return 'failure'; + return wrap('failure', message: jsonData); + } catch (_) { + return wrap('failure', message: response.body); } } else { - // addError('Non-200 response code: ${response.statusCode}', - // 'crud().post - Other'); - return 'failure'; + try { + final jsonData = jsonDecode(response.body); + return wrap('failure', message: jsonData); + } catch (_) { + return wrap('failure', message: response.body); + } } } catch (e) { - // addError('HTTP request error: $e', 'crud().post - HTTP'); - return 'failure'; + return { + 'status': 'failure', + 'message': 'HTTP request error: $e', + 'code': -1 + }; } } @@ -248,6 +336,7 @@ class CRUD { required String link, Map? payload, }) async { + // Uses Basic Auth, so it's a separate implementation. var url = Uri.parse( link, ); @@ -261,14 +350,10 @@ class CRUD { }, ); if (response.statusCode == 200) { - var jsonData = jsonDecode(response.body); - // if (jsonData['status'] == 'success') { - - return jsonData; - // } - - // return jsonData['status']; + return jsonDecode(response.body); } + // Consider adding error handling here. + return null; } Future sendWhatsAppAuth(String to, String token) async { @@ -706,4 +791,7 @@ class CRUD { ); return json.decode(response.body); } + + // ... [Other methods like sendWhatsAppAuth, getAgoraToken, getLlama, etc., would remain here as they are] ... + // For brevity, I am omitting the rest of the third-party API methods as they would not change. } diff --git a/lib/controller/functions/network/connection_check.dart b/lib/controller/functions/network/connection_check.dart new file mode 100644 index 0000000..82bc47b --- /dev/null +++ b/lib/controller/functions/network/connection_check.dart @@ -0,0 +1,48 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:http/http.dart' as http; + +import 'net_guard.dart'; + +typedef BodyEncoder = Future Function(); + +class HttpRetry { + /// ريتراي لـ network/transient errors فقط. + static Future 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); + } + } +} diff --git a/lib/controller/functions/network/net_guard.dart b/lib/controller/functions/network/net_guard.dart new file mode 100644 index 0000000..7c4c1f1 --- /dev/null +++ b/lib/controller/functions/network/net_guard.dart @@ -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 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); + } +} diff --git a/lib/controller/home/map_passenger_controller.dart b/lib/controller/home/map_passenger_controller.dart index 533e6db..e359ac2 100644 --- a/lib/controller/home/map_passenger_controller.dart +++ b/lib/controller/home/map_passenger_controller.dart @@ -6,6 +6,7 @@ import 'dart:math' as math; import 'dart:ui'; import 'dart:convert'; import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/services.dart'; import 'package:http/http.dart' as http; import 'package:Intaleq/constant/univeries_polygon.dart'; @@ -13,6 +14,7 @@ import 'package:Intaleq/controller/firebase/local_notification.dart'; import 'package:Intaleq/controller/functions/encrypt_decrypt.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_confetti/flutter_confetti.dart'; +import 'package:uni_links/uni_links.dart'; import 'package:vector_math/vector_math.dart' show radians, degrees; import 'package:Intaleq/controller/functions/tts.dart'; @@ -56,6 +58,10 @@ import 'device_tier.dart'; import 'vip_waitting_page.dart'; class MapPassengerController extends GetxController { + // --- START: DEEP LINKING ADDITIONS --- + StreamSubscription? _linkSubscription; + // --- END: DEEP LINKING ADDITIONS --- + bool isLoading = true; TextEditingController placeDestinationController = TextEditingController(); TextEditingController increasFeeFromPassenger = TextEditingController(); @@ -280,6 +286,92 @@ class MapPassengerController extends GetxController { update(); } + /// Initializes the deep link listener. + /// It checks for the initial link when the app starts and then listens for subsequent links. + Future _initUniLinks() async { + try { + // Get the initial link that opened the app + final initialLink = await getInitialUri(); + if (initialLink != null) { + handleDeepLink(initialLink); + } + } on PlatformException { + print('Failed to get initial deep link.'); + } on FormatException { + print('Invalid initial deep link format.'); + } + + // Listen for incoming links while the app is running + _linkSubscription = uriLinkStream.listen((Uri? link) { + handleDeepLink(link); + }, onError: (err) { + print('Error listening to deep links: $err'); + }); + } + + /// Parses the incoming deep link and triggers the route initiation. + void handleDeepLink(Uri? link) { + if (link == null) return; + + // Check if the link matches your app's scheme and path + // e.g., intaleq://map?lat=31.9539&lng=35.9106 + if (link.scheme == 'intaleq' && link.host == 'map') { + final latString = link.queryParameters['lat']; + final lngString = link.queryParameters['lng']; + + if (latString != null && lngString != null) { + final double? lat = double.tryParse(latString); + final double? lng = double.tryParse(lngString); + + if (lat != null && lng != null) { + final destination = LatLng(lat, lng); + print('Deep link received. Destination: $destination'); + initiateRouteFromDeepLink(destination); + } else { + print('Failed to parse lat/lng from deep link.'); + } + } + } + } + + /// Sets the destination from the deep link and updates the UI to show the map. + void initiateRouteFromDeepLink(LatLng destination) async { + // Wait for map controller to be ready + if (mapController == null) { + await Future.delayed(const Duration(seconds: 1)); + if (mapController == null) { + print("Map controller is not available to handle deep link."); + return; + } + } + + myDestination = destination; + + // Animate camera to user's current location to show the starting point + await mapController?.animateCamera(CameraUpdate.newLatLng( + LatLng(passengerLocation.latitude, passengerLocation.longitude))); + + // Ensure the main menu is visible to start the booking process + if (isMainBottomMenuMap) { + changeMainBottomMenuMap(); + } + + passengerStartLocationFromMap = true; + isPickerShown = true; + hintTextDestinationPoint = "Destination from external link".tr; + update(); + + // The user can now see the destination and proceed to get the route and price. + Get.snackbar( + "Location Received".tr, + "The destination has been set from the link.".tr, + backgroundColor: AppColor.greenColor, + colorText: Colors.white, + ); + } + + // --- END: DEEP LINKING METHODS --- + void getCurrentLocationFormString() async { currentLocationToFormPlaces = true; currentLocationString = 'Waiting for your location'.tr; @@ -3190,6 +3282,8 @@ class MapPassengerController extends GetxController { print( "--- MapPassengerController: Closing and cleaning up all resources. ---"); + _linkSubscription?.cancel(); + // 1. إلغاء المؤقتات الفردية // Using ?.cancel() is safe even if the timer is null markerReloadingTimer.cancel(); @@ -5719,6 +5813,7 @@ class MapPassengerController extends GetxController { await initilizeGetStorage(); // إعداد سريع await _initMinimalIcons(); // start/end فقط await addToken(); // لو لازم للمصادقة + await _initUniLinks(); await getLocation(); // لتحديد الكاميرا box.write(BoxName.carType, 'yet'); box.write(BoxName.tipPercentage, '0'); diff --git a/lib/controller/local/translations.dart b/lib/controller/local/translations.dart index 4925bef..edefc8b 100644 --- a/lib/controller/local/translations.dart +++ b/lib/controller/local/translations.dart @@ -179,6 +179,13 @@ class MyTranslation extends Translations { "Contacts Loaded": "تم تحميل جهات الاتصال", "Showing": "يتم عرض", "of": "من", + "Customer not found": "العميل غير موجود", + "Wallet is blocked": "المحفظة محظورة", + "Customer phone is not active": "هاتف العميل غير نشط", + "Balance not enough": "الرصيد غير كافٍ", + "Balance limit exceeded": "تم تجاوز حد الرصيد", + "Incorrect sms code": + "⚠️ رمز التحقق الذي أدخلته غير صحيح. يرجى المحاولة مرة أخرى.", "contacts. Others were hidden because they don't have a phone number.": "جهة اتصال. تم إخفاء البقية لعدم وجود أرقام هواتف لديهم.", "No contacts found": "لم يتم العثور على جهات اتصال", @@ -1363,6 +1370,8 @@ class MyTranslation extends Translations { "Edit Your data": "تعديل بياناتك", "write vin for your car": "اكتب رقم هيكل سيارتك", "VIN": "رقم الهيكل", + "Device Change Detected": "تم اكتشاف تغيير في الجهاز", + "Please verify your identity": "يرجى التحقق من هويتك", "write Color for your car": "اكتب لون سيارتك", "write Make for your car": "اكتب الشركة المصنعة لسيارتك", "write Model for your car": "اكتب موديل سيارتك", @@ -1458,6 +1467,19 @@ class MyTranslation extends Translations { "يرجى البقاء في نقطة الالتقاط المحددة.", "message From Driver": "رسالة من السائق", "Trip is Begin": "بدأت الرحلة", + "Verify OTP": "التحقق من الرمز", + "Customer not found": "العميل غير موجود", + "Wallet is blocked": "المحفظة محظورة", + "Customer phone is not active": "هاتف العميل غير نشط", + "Balance not enough": "الرصيد غير كافٍ", + "Balance limit exceeded": "تم تجاوز حد الرصيد", + "Verification Code": "رمز التحقق", + "We have sent a verification code to your mobile number:": + "لقد أرسلنا رمز التحقق إلى رقم هاتفك المحمول:", + "Verify": "تحقق", + "Resend Code": "إعادة إرسال الرمز", + "You can resend in": "يمكنك إعادة الإرسال خلال", + "seconds": "ثوانٍ", "Cancel Trip from driver": "إلغاء الرحلة من السائق", "We will look for a new driver.\nPlease wait.": "هنبحث عن سائق جديد.\nمن فضلك انتظر.", diff --git a/lib/controller/payment/payment_controller.dart b/lib/controller/payment/payment_controller.dart index 580c313..aff1439 100644 --- a/lib/controller/payment/payment_controller.dart +++ b/lib/controller/payment/payment_controller.dart @@ -664,154 +664,154 @@ class PaymentController extends GetxController { Future payWithMTNWallet( BuildContext context, String amount, String currency) async { - // استخدام مؤشر تحميل لتجربة مستخدم أفضل - Get.dialog(const Center(child: CircularProgressIndicator()), - barrierDismissible: false); + // خزن سياق علوي آمن من البداية + final BuildContext safeContext = + Get.overlayContext ?? Get.context ?? context; + + // سبينر تحميل + if (!(Get.isDialogOpen ?? false)) { + Get.dialog(const Center(child: CircularProgressIndicator()), + barrierDismissible: false); + } try { - String phone = box.read(BoxName.phoneWallet); - String passengerID = box.read(BoxName.passengerID).toString(); - String formattedAmount = double.parse(amount).toStringAsFixed(0); + final phone = box.read(BoxName.phoneWallet) as String; + final passengerID = box.read(BoxName.passengerID).toString(); + final formattedAmount = double.parse(amount).toStringAsFixed(0); print("🚀 بدء عملية دفع MTN"); print( "📦 Payload: passengerID: $passengerID, amount: $formattedAmount, phone: $phone"); - // التحقق من البصمة (اختياري) - bool isAuthSupported = await LocalAuthentication().isDeviceSupported(); + // التحقق بالبصمة (اختياري) + حماية من الـ await + final localAuth = LocalAuthentication(); + final isAuthSupported = await localAuth.isDeviceSupported(); if (isAuthSupported) { - bool didAuthenticate = await LocalAuthentication().authenticate( + final didAuth = await localAuth.authenticate( localizedReason: 'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع', ); - if (!didAuthenticate) { - if (Get.isDialogOpen ?? false) Get.back(); + if (!didAuth) { + if (Get.isDialogOpen == true) Get.back(); print("❌ المستخدم لم يؤكد بالبصمة/الوجه"); return; } } - // 1️⃣ استدعاء mtn_start_payment.php (الملف الجديد) - var responseData = await CRUD().postWallet( + // 1) بدء الدفع + final responseData = await CRUD().postWalletMtn( link: AppLink.payWithMTNStart, payload: { "amount": formattedAmount, "passengerId": passengerID, "phone": phone, + "lang": box.read(BoxName.lang) ?? 'ar', }, ); - print("✅ استجابة الخادم (mtn_start_payment.php):"); - print(responseData); - - // --- بداية التعديل المهم --- - // التحقق القوي من الاستجابة لتجنب الأخطاء - Map startRes; + // print("✅ استجابة الخادم (mtn_start_payment.php):"); + // print(responseData); + Log.print('responseData: ${responseData}'); + // فحص الاستجابة بقوة + late final Map startRes; if (responseData is Map) { - // إذا كانت الاستجابة بالفعل Map، استخدمها مباشرة startRes = responseData; } else if (responseData is String) { - // إذا كانت نص، حاول تحليلها كـ JSON - try { - startRes = json.decode(responseData); - } catch (e) { - throw Exception( - "فشل في تحليل استجابة الخادم. الاستجابة: $responseData"); - } + startRes = json.decode(responseData) as Map; } else { - // نوع غير متوقع throw Exception("تم استلام نوع بيانات غير متوقع من الخادم."); } if (startRes['status'] != 'success') { - String errorMsg = startRes['message']?.toString() ?? + final errorMsg = startRes['message']['Error']?.toString().tr ?? "فشل بدء عملية الدفع. حاول مرة أخرى."; throw Exception(errorMsg); } - // --- نهاية التعديل المهم --- - // استخراج البيانات بأمان - final messageData = startRes["message"]; + final messageData = startRes["message"] as Map; final invoiceNumber = messageData["invoiceNumber"].toString(); final operationNumber = messageData["operationNumber"].toString(); final guid = messageData["guid"].toString(); - print( - "📄 invoiceNumber: $invoiceNumber, 🔢 operationNumber: $operationNumber, 🧭 guid: $guid"); + // print( + // "📄 invoiceNumber: $invoiceNumber, 🔢 operationNumber: $operationNumber, 🧭 guid: $guid"); - if (Get.isDialogOpen ?? false) - Get.back(); // إغلاق مؤشر التحميل قبل عرض حوار OTP + // أغلق السبينر قبل إظهار حوار OTP + if (Get.isDialogOpen == true) Get.back(); - // 2️⃣ عرض واجهة إدخال OTP - String? otp = await showDialog( - context: context, - 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(), - ), - ], - ); - }, - ); + // 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 صحيح (4–8 أرقام)"); + 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 (otp == null || otp.isEmpty) { + if (otpInput.isEmpty) { print("❌ لم يتم إدخال OTP"); return; } - print("🔐 تم إدخال OTP: $otp"); + print("🔐 تم إدخال OTP: $otpInput"); + // سبينر أثناء التأكيد Get.dialog(const Center(child: CircularProgressIndicator()), barrierDismissible: false); - // 3️⃣ استدعاء mtn_confirm.php - var confirmRes = await CRUD().postWallet( + // 3) تأكيد الدفع + final confirmRes = await CRUD().postWalletMtn( link: AppLink.payWithMTNConfirm, payload: { "invoiceNumber": invoiceNumber, "operationNumber": operationNumber, "guid": guid, - "otp": otp, + "otp": otpInput, "phone": phone, + "lang": box.read(BoxName.lang) ?? 'ar', }, ); - if (Get.isDialogOpen ?? false) Get.back(); + if (Get.isDialogOpen == true) Get.back(); - print("✅ استجابة mtn_confirm.php:"); - print(confirmRes); + // print("✅ استجابة mtn_confirm.php:"); + // Log.print('confirmRes: ${confirmRes}'); - if (confirmRes != null && confirmRes['status'] == 'success') { + final ok = (confirmRes is Map && confirmRes['status'] == 'success'); + if (ok) { Get.defaultDialog( title: "✅ نجاح", content: const Text("تمت عملية الدفع وإضافة الرصيد إلى محفظتك."), ); + await getPassengerWallet(); } else { - String errorMsg = - confirmRes?['message']?.toString() ?? "فشل في تأكيد الدفع"; - Get.defaultDialog( - title: "❌ فشل", - content: Text(errorMsg), - ); + final errorMsg = (confirmRes['message']['message']?.toString()) ?? + "فشل في تأكيد الدفع"; + Get.defaultDialog(title: "❌ فشل", content: Text(errorMsg.tr)); } } catch (e, s) { print("🔥 خطأ أثناء الدفع عبر MTN:"); print(e); print(s); - if (Get.isDialogOpen ?? false) Get.back(); + if (Get.isDialogOpen == true) Get.back(); Get.defaultDialog( title: 'حدث خطأ', content: Text(e.toString().replaceFirst("Exception: ", "")), diff --git a/lib/main.dart b/lib/main.dart index a978ab6..c5d9880 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,8 @@ +import 'dart:async'; import 'dart:io'; import 'dart:math'; +import 'package:Intaleq/controller/functions/crud.dart'; import 'package:Intaleq/controller/payment/paymob/paymob_response.dart'; import 'package:Intaleq/views/home/HomePage/contact_us.dart'; import 'package:Intaleq/views/home/HomePage/share_app_page.dart'; @@ -133,7 +135,30 @@ void main() async { ), ]); - runApp(const MyApp()); + runZonedGuarded>(() async { + runApp(const MyApp()); + }, (error, stack) { + // ==== START: ERROR FILTER ==== + String errorString = error.toString(); + + // Print all errors to the local debug console for development + print("Caught Dart error: $error"); + print(stack); + + // 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 ==== + }); } class MyApp extends StatelessWidget { diff --git a/lib/views/home/my_wallet/passenger_wallet_dialoge.dart b/lib/views/home/my_wallet/passenger_wallet_dialoge.dart index 610f426..dcd541b 100644 --- a/lib/views/home/my_wallet/passenger_wallet_dialoge.dart +++ b/lib/views/home/my_wallet/passenger_wallet_dialoge.dart @@ -9,6 +9,8 @@ import 'package:Intaleq/controller/functions/toast.dart'; import 'package:Intaleq/controller/payment/payment_controller.dart'; import '../../../main.dart'; +import '../../widgets/elevated_btn.dart'; +import '../../widgets/my_textField.dart'; class PassengerWalletDialog extends StatelessWidget { const PassengerWalletDialog({ @@ -264,76 +266,143 @@ void showPaymentOptions(BuildContext context, PaymentController controller) { }, ) : const SizedBox(), - box.read(BoxName.phoneWallet) != null - ? CupertinoActionSheetAction( - child: Text('💰 Pay with Wallet'.tr), + // box.read(BoxName.phoneWallet) != null + // ? CupertinoActionSheetAction( + // child: Text('💰 Pay with Wallet'.tr), + // onPressed: () async { + // if (controller.selectedAmount != 0) { + // controller.isLoading = true; + // controller.update(); + // controller.payWithMTNWallet( + // context, + // controller.selectedAmount.toString(), + // 'SYP', + // ); + // await controller.getPassengerWallet(); + // controller.isLoading = false; + // controller.update(); + // } else { + // Toast.show(context, '⚠️ You need to choose an amount!'.tr, + // AppColor.redColor); + // } + // }, + // ) + // : CupertinoActionSheetAction( + // child: Text('Add wallet phone you use'.tr), + // onPressed: () { + // Get.dialog( + // CupertinoAlertDialog( + // title: Text('Insert Wallet phone number'.tr), + // content: Column( + // children: [ + // const SizedBox(height: 10), + // CupertinoTextField( + // controller: controller.walletphoneController, + // placeholder: 'Insert Wallet phone number'.tr, + // keyboardType: TextInputType.phone, + // padding: const EdgeInsets.symmetric( + // vertical: 12, + // horizontal: 10, + // ), + // ), + // ], + // ), + // actions: [ + // CupertinoDialogAction( + // child: Text('Cancel'.tr, + // style: const TextStyle( + // color: CupertinoColors.destructiveRed)), + // onPressed: () { + // Get.back(); + // }, + // ), + // CupertinoDialogAction( + // child: Text('OK'.tr, + // style: const TextStyle( + // color: CupertinoColors.activeGreen)), + // onPressed: () async { + // Get.back(); + // box.write(BoxName.phoneWallet, + // (controller.walletphoneController.text)); + // Toast.show( + // context, + // 'Phone Wallet Saved Successfully'.tr, + // AppColor.greenColor); + // }, + // ), + // ], + // ), + // barrierDismissible: false, + // ); + // }, + // ), + GestureDetector( + onTap: () async { + Get.back(); + // final formKey = GlobalKey(); + // final phoneController = TextEditingController(); + + Get.defaultDialog( + barrierDismissible: false, + title: 'Insert Wallet phone number'.tr, + content: Form( + key: controller.formKey, + child: TextFormField( + controller: controller.walletphoneController, + keyboardType: TextInputType.phone, + decoration: InputDecoration( + labelText: 'Insert Wallet phone number'.tr, + hintText: '963941234567', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return '⚠️ Please enter phone number'.tr; + } else if (value.length != 12) { + return '⚠️ Phone number must be 12 digits'.tr; + } + return null; + }, + ), + ), + confirm: ElevatedButton( + child: Text('OK'.tr), onPressed: () async { - if (controller.selectedAmount != 0) { - controller.isLoading = true; - controller.update(); - controller.payWithMTNWallet( - context, - controller.selectedAmount.toString(), - 'SYP', - ); - await controller.getPassengerWallet(); - controller.isLoading = false; - controller.update(); - } else { - Toast.show(context, '⚠️ You need to choose an amount!'.tr, - AppColor.redColor); + if (controller.formKey.currentState!.validate()) { + if (controller.selectedAmount != 0) { + controller.isLoading = true; + controller.update(); + box.write(BoxName.phoneWallet, + (controller.walletphoneController.text)); + Get.back(); + await controller.payWithMTNWallet( + context, + controller.selectedAmount.toString(), + 'SYP', + ); + await controller.getPassengerWallet(); + + controller.isLoading = false; + controller.update(); + } else { + Toast.show( + context, + '⚠️ You need to choose an amount!'.tr, + AppColor.redColor, + ); + } } }, - ) - : CupertinoActionSheetAction( - child: Text('Add wallet phone you use'.tr), - onPressed: () { - Get.dialog( - CupertinoAlertDialog( - title: Text('Insert Wallet phone number'.tr), - content: Column( - children: [ - const SizedBox(height: 10), - CupertinoTextField( - controller: controller.walletphoneController, - placeholder: 'Insert Wallet phone number'.tr, - keyboardType: TextInputType.phone, - padding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 10, - ), - ), - ], - ), - actions: [ - CupertinoDialogAction( - child: Text('Cancel'.tr, - style: const TextStyle( - color: CupertinoColors.destructiveRed)), - onPressed: () { - Get.back(); - }, - ), - CupertinoDialogAction( - child: Text('OK'.tr, - style: const TextStyle( - color: CupertinoColors.activeGreen)), - onPressed: () async { - Get.back(); - box.write(BoxName.phoneWallet, - (controller.walletphoneController.text)); - Toast.show( - context, - 'Phone Wallet Saved Successfully'.tr, - AppColor.greenColor); - }, - ), - ], - ), - barrierDismissible: false, - ); - }, ), + ); + }, + child: Image.asset( + 'assets/images/mtn.png', + width: 70, + height: 70, + fit: BoxFit.contain, + ), + ) ], cancelButton: CupertinoActionSheetAction( child: Text('Cancel'.tr), diff --git a/pubspec.lock b/pubspec.lock index 34b2a6d..44d64d7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -233,6 +233,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec + url: "https://pub.dev" + source: hosted + version: "6.1.5" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" + url: "https://pub.dev" + source: hosted + version: "2.0.1" convert: dependency: transitive description: @@ -1112,6 +1128,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1+1" + internet_connection_checker: + dependency: "direct main" + description: + name: internet_connection_checker + sha256: ee08f13d8b13b978affe226e9274ca3ba7a9bed07c9479e8ae245f785b7a488a + url: "https://pub.dev" + source: hosted + version: "3.0.1" intl: dependency: "direct main" description: @@ -1336,6 +1360,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" octo_image: dependency: transitive description: @@ -1908,6 +1940,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + uni_links: + dependency: "direct main" + description: + name: uni_links + sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + uni_links_platform_interface: + dependency: transitive + description: + name: uni_links_platform_interface + sha256: "929cf1a71b59e3b7c2d8a2605a9cf7e0b125b13bc858e55083d88c62722d4507" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + uni_links_web: + dependency: transitive + description: + name: uni_links_web + sha256: "7539db908e25f67de2438e33cc1020b30ab94e66720b5677ba6763b25f6394df" + url: "https://pub.dev" + source: hosted + version: "0.1.0" url_launcher: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 8f1cbe1..a3a4edf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,7 +59,7 @@ dependencies: sign_in_with_apple: ^6.1.0 firebase_auth: ^5.1.2 device_info_plus: ^11.3.0 - # uni_links: ^0.5.1 + uni_links: ^0.5.1 googleapis_auth: ^1.6.0 flutter_confetti: ^0.3.0 # intl_phone_field: ^3.1.0 @@ -75,6 +75,8 @@ dependencies: shimmer: ^3.0.0 share_plus: ^11.0.0 asn1lib: ^1.6.5 + internet_connection_checker: ^3.0.1 + connectivity_plus: ^6.1.5 # home_widget: ^0.7.0+1 dev_dependencies: