Initial push to my private server

This commit is contained in:
Hamza-Ayed
2025-09-09 22:40:27 +03:00
parent d677ab957a
commit 13d77e118c
20 changed files with 921 additions and 452 deletions

View File

@@ -47,8 +47,8 @@ android {
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 29 minSdk = 29
targetSdk = 36 targetSdk = 36
versionCode = 11 versionCode = 13
versionName = '1.0.11' versionName = '1.0.13'
multiDexEnabled = true multiDexEnabled = true
ndk { ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"

View File

@@ -43,15 +43,15 @@
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<!-- Add this new intent filter for deep linking -->
<intent-filter android:autoVerify="true">
<!-- Deep Linking Intent Filter -->
<intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<!-- Accepts URIs that begin with "sefer://" --> <!-- Accepts the custom scheme 'intaleq' and host 'map' -->
<data android:scheme="sefer" /> <data android:scheme="intaleq" android:host="map" />
<!-- Accepts URIs that begin with "https://sefer.live" -->
<data android:scheme="https" android:host="sefer.live" />
</intent-filter> </intent-filter>
</activity> </activity>

View File

@@ -30,7 +30,7 @@ buildscript {
// classpath 'com.android.tools.build:gradle:7.3.1' // classpath 'com.android.tools.build:gradle:7.3.1'
classpath 'com.google.gms:google-services:4.3.15' classpath 'com.google.gms:google-services:4.3.15'
// END: FlutterFire Configuration // 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" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }

File diff suppressed because one or more lines are too long

View File

@@ -18,7 +18,7 @@ pluginManagement {
plugins { plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" 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 // START: FlutterFire Configuration
id "com.google.gms.google-services" version "4.3.10" apply false id "com.google.gms.google-services" version "4.3.10" apply false
// END: FlutterFire Configuration // END: FlutterFire Configuration

BIN
assets/images/mtn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -11,6 +11,8 @@ PODS:
- PromisesObjC (~> 2.4) - PromisesObjC (~> 2.4)
- audio_session (0.0.1): - audio_session (0.0.1):
- Flutter - Flutter
- connectivity_plus (0.0.1):
- Flutter
- device_info_plus (0.0.1): - device_info_plus (0.0.1):
- Flutter - Flutter
- Firebase/Auth (11.15.0): - Firebase/Auth (11.15.0):
@@ -251,6 +253,7 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- audio_session (from `.symlinks/plugins/audio_session/ios`) - 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`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
@@ -323,6 +326,8 @@ SPEC REPOS:
EXTERNAL SOURCES: EXTERNAL SOURCES:
audio_session: audio_session:
:path: ".symlinks/plugins/audio_session/ios" :path: ".symlinks/plugins/audio_session/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus: device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios" :path: ".symlinks/plugins/device_info_plus/ios"
firebase_auth: firebase_auth:
@@ -396,6 +401,7 @@ SPEC CHECKSUMS:
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0 audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e
firebase_auth: 50af8366c87bb88c80ebeae62eb60189c7246b9b firebase_auth: 50af8366c87bb88c80ebeae62eb60189c7246b9b

View File

@@ -322,14 +322,10 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Copy Pods Resources"; name = "[CP] Copy Pods Resources";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -343,14 +339,10 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";

View File

@@ -1,100 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CFBundleURLTypes</key>
<true/> <array>
<key>CFBundleDevelopmentRegion</key> <dict>
<string>$(DEVELOPMENT_LANGUAGE)</string> <key>CFBundleTypeRole</key>
<key>CFBundleDisplayName</key> <string>Editor</string>
<string>Intaleq</string> <key>CFBundleURLName</key>
<key>CFBundleExecutable</key> <!-- استبدل هذا بمعرّف الحزمة الخاص بك -->
<string>$(EXECUTABLE_NAME)</string> <string>com.intaleq.app</string>
<key>CFBundleIdentifier</key> <key>CFBundleURLSchemes</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <array>
<key>CFBundleInfoDictionaryVersion</key> <string>intaleq</string>
<string>6.0</string> </array>
<key>CFBundleName</key> </dict>
<string>Intaleq</string> </array>
<key>CFBundlePackageType</key> <key>FlutterDeepLinkingEnabled</key>
<string>APPL</string> <true />
<key>CFBundleShortVersionString</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<string>6</string> <true />
<key>CFBundleSignature</key> <key>CFBundleDevelopmentRegion</key>
<string>????</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleURLTypes</key> <key>CFBundleDisplayName</key>
<array> <string>Intaleq</string>
<dict> <key>CFBundleExecutable</key>
<key>CFBundleTypeRole</key> <string>$(EXECUTABLE_NAME)</string>
<string>Editor</string> <key>CFBundleIdentifier</key>
<key>CFBundleURLSchemes</key> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<array> <key>CFBundleInfoDictionaryVersion</key>
<string>com.googleusercontent.apps.1086900987150-9jv4oa8l3t23d54lrf27c1d22tbt9i6d</string> <string>6.0</string>
</array> <key>CFBundleName</key>
</dict> <string>Intaleq</string>
<dict/> <key>CFBundlePackageType</key>
</array> <string>APPL</string>
<key>CFBundleVersion</key> <key>CFBundleShortVersionString</key>
<string>1.0.6</string> <string>7</string>
<key>FirebaseAppDelegateProxyEnabled</key> <key>CFBundleSignature</key>
<string>NO</string> <string>????</string>
<key>GMSApiKey</key> <key>CFBundleURLTypes</key>
<string>YOUR_API_KEY</string> <array>
<key>LSApplicationQueriesSchemes</key> <dict>
<array> <key>CFBundleTypeRole</key>
<string>googlechromes</string> <string>Editor</string>
<string>comgooglemaps</string> <key>CFBundleURLSchemes</key>
</array> <array>
<key>LSRequiresIPhoneOS</key> <string>
<true/> com.googleusercontent.apps.1086900987150-9jv4oa8l3t23d54lrf27c1d22tbt9i6d</string>
<key>NSCameraUsageDescription</key> </array>
<string>This app requires access to your camera in order to scan QR codes and capture images </dict>
<dict />
</array>
<key>CFBundleVersion</key>
<string>1.0.7</string>
<key>FirebaseAppDelegateProxyEnabled</key>
<string>NO</string>
<key>GMSApiKey</key>
<string>YOUR_API_KEY</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>googlechromes</string>
<string>comgooglemaps</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true />
<key>NSCameraUsageDescription</key>
<string>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.</string> for uploading and access to connect to a call.</string>
<key>NSContactsUsageDescription</key> <key>NSContactsUsageDescription</key>
<string>This app requires contacts access to function properly.</string> <string>This app requires contacts access to function properly.</string>
<key>NSFaceIDUsageDescription</key> <key>NSFaceIDUsageDescription</key>
<string>Use Face ID to securely authenticate payment accounts.</string> <string>Use Face ID to securely authenticate payment accounts.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key> <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs access to your location to provide you with the best ride experience. <string>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 Your location data will be used to find the nearest available cars and connect you with
the closest captain for efficient and convenient rides.</string> the closest captain for efficient and convenient rides.</string>
<key>NSLocationAlwaysUsageDescription</key> <key>NSLocationAlwaysUsageDescription</key>
<string>This app needs access to location.</string> <string>This app needs access to location.</string>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs access to your location to provide you with the best ride experience. <string>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 Your location data will be used to find the nearest available cars and connect you with
the closest captain for efficient and convenient rides.</string> the closest captain for efficient and convenient rides.</string>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>This app requires access to your microphone to record audio, allowing you to add <string>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.</string> voice recordings to your photos and videos and access to connect to a call.</string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>This app requires access to the photo library to upload pictures.</string> <string>This app requires access to the photo library to upload pictures.</string>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true />
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>fetch</string> <string>fetch</string>
<string>location</string> <string>location</string>
<string>remote-notification</string> <string>remote-notification</string>
</array> </array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
<string>Main</string> <string>Main</string>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UISupportedInterfaceOrientations~ipad</key> <key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false />
</dict> </dict>
</plist> </plist>

View File

@@ -242,6 +242,7 @@ class LoginController extends GetxController {
if ((jsonDecode(token)['message']['token'].toString()) != if ((jsonDecode(token)['message']['token'].toString()) !=
box.read(BoxName.tokenFCM)) { box.read(BoxName.tokenFCM)) {
await Get.defaultDialog( await Get.defaultDialog(
barrierDismissible: false,
title: 'Device Change Detected'.tr, title: 'Device Change Detected'.tr,
middleText: 'Please verify your identity'.tr, middleText: 'Please verify your identity'.tr,
textConfirm: 'Verify'.tr, textConfirm: 'Verify'.tr,

View File

@@ -9,238 +9,326 @@ import 'package:Intaleq/env/env.dart';
import '../../constant/api_key.dart'; import '../../constant/api_key.dart';
import '../../print.dart';
import '../../views/widgets/elevated_btn.dart'; import '../../views/widgets/elevated_btn.dart';
import '../../views/widgets/error_snakbar.dart'; import '../../views/widgets/error_snakbar.dart';
import 'add_error.dart';
import 'encrypt_decrypt.dart'; import 'encrypt_decrypt.dart';
import 'upload_image.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 { 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<void> addError(
String error, String details, String where) async {
try {
final currentErrorSignature = '$where-$error';
final now = DateTime.now();
if (currentErrorSignature == _lastErrorSignature &&
now.difference(_lastErrorTimestamp) < _errorLogDebounceDuration) {
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<dynamic> _makeRequest({
required String link,
Map<String, dynamic>? payload,
required Map<String, String> 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<dynamic> post({
required String link,
Map<String, dynamic>? 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<dynamic> get({ Future<dynamic> get({
required String link, required String link,
Map<String, dynamic>? payload, Map<String, dynamic>? payload,
}) async { }) async {
// print(r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]); String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
var url = Uri.parse( if (JwtDecoder.isExpired(token)) {
link, await Get.put(LoginController()).getJWT();
); token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
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';
} }
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<dynamic> postWallet({
required String link,
Map<String, dynamic>? payload,
}) async {
var jwt = await LoginController().getJwtWallet();
final hmac = box.read(BoxName.hmac);
final headers = {
"Content-Type": "application/x-www-form-urlencoded",
'Authorization': 'Bearer $jwt',
'X-HMAC-Auth': hmac.toString(),
};
return await _makeRequest(
link: link,
payload: payload,
headers: headers,
);
}
/// Performs an authenticated GET request to wallet endpoints (using POST).
Future<dynamic> getWallet({ Future<dynamic> getWallet({
required String link, required String link,
Map<String, dynamic>? payload, Map<String, dynamic>? payload,
}) async { }) async {
var s = await LoginController().getJwtWallet(); var jwt = await LoginController().getJwtWallet();
final hmac = box.read(BoxName.hmac); final hmac = box.read(BoxName.hmac);
// Log.print('hmac: ${hmac}');
var url = Uri.parse( final headers = {
link, "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']; if (result is Map && result['status'] == 'success') {
} else if (response.statusCode == 401) { return jsonEncode(result);
// 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';
} }
return result;
} }
Future<dynamic> 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<dynamic> postWalletMtn(
{required String link, Map<String, dynamic>? payload}) async { {required String link, Map<String, dynamic>? 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 { try {
var response = await http.post( final response = await http.post(
url, url,
body: payload, body: payload,
headers: { headers: {
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
'Authorization': "Authorization": "Bearer $s",
'Bearer ${r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]}' "X-HMAC-Auth": hmac.toString(),
}, },
); );
// Log.print('req: ${response.request}');
// Log.print('response: ${response.body}'); print('req: ${response.request}');
// Log.print('payload: ${payload}'); print('status: ${response.statusCode}');
print('body: ${response.body}');
print('payload: $payload');
Map<String, dynamic> wrap(String status, {Object? message, int? code}) {
return {
'status': status,
'message': message,
'code': code ?? response.statusCode,
};
}
if (response.statusCode == 200) { if (response.statusCode == 200) {
try { try {
var jsonData = jsonDecode(response.body); return jsonDecode(response.body);
if (jsonData['status'] == 'success') {
return jsonData;
} else {
return jsonData['status'];
}
} catch (e) { } catch (e) {
// addError(e.toString(), 'crud().post - JSON decoding'); return wrap('failure',
return 'failure'; message: 'JSON decode error', code: response.statusCode);
} }
} else if (response.statusCode == 401) { } 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<dynamic> postWallet(
{required String link, Map<String, dynamic>? 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 { try {
var jsonData = jsonDecode(response.body); final jsonData = jsonDecode(response.body);
if (jsonData['status'] == 'success') { if (jsonData is Map && jsonData['error'] == 'Token expired') {
return jsonData; await Get.put(LoginController()).getJWT();
} else { return {
return jsonData['status']; 'status': 'failure',
'message': 'token_expired',
'code': 401
};
} }
} catch (e) { return wrap('failure', message: jsonData);
addError(e.toString(), 'crud().post - JSON decoding'); } catch (_) {
return 'failure'; return wrap('failure', message: response.body);
}
} 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 { } else {
// addError('Non-200 response code: ${response.statusCode}', try {
// 'crud().post - Other'); final jsonData = jsonDecode(response.body);
return 'failure'; return wrap('failure', message: jsonData);
} catch (_) {
return wrap('failure', message: response.body);
}
} }
} catch (e) { } catch (e) {
// addError('HTTP request error: $e', 'crud().post - HTTP'); return {
return 'failure'; 'status': 'failure',
'message': 'HTTP request error: $e',
'code': -1
};
} }
} }
@@ -248,6 +336,7 @@ class CRUD {
required String link, required String link,
Map<String, dynamic>? payload, Map<String, dynamic>? payload,
}) async { }) async {
// Uses Basic Auth, so it's a separate implementation.
var url = Uri.parse( var url = Uri.parse(
link, link,
); );
@@ -261,14 +350,10 @@ class CRUD {
}, },
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
var jsonData = jsonDecode(response.body); return jsonDecode(response.body);
// if (jsonData['status'] == 'success') {
return jsonData;
// }
// return jsonData['status'];
} }
// Consider adding error handling here.
return null;
} }
Future sendWhatsAppAuth(String to, String token) async { Future sendWhatsAppAuth(String to, String token) async {
@@ -706,4 +791,7 @@ class CRUD {
); );
return json.decode(response.body); 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.
} }

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import 'dart:math' as math;
import 'dart:ui'; import 'dart:ui';
import 'dart:convert'; import 'dart:convert';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:Intaleq/constant/univeries_polygon.dart'; 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:Intaleq/controller/functions/encrypt_decrypt.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter_confetti/flutter_confetti.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:vector_math/vector_math.dart' show radians, degrees;
import 'package:Intaleq/controller/functions/tts.dart'; import 'package:Intaleq/controller/functions/tts.dart';
@@ -56,6 +58,10 @@ import 'device_tier.dart';
import 'vip_waitting_page.dart'; import 'vip_waitting_page.dart';
class MapPassengerController extends GetxController { class MapPassengerController extends GetxController {
// --- START: DEEP LINKING ADDITIONS ---
StreamSubscription? _linkSubscription;
// --- END: DEEP LINKING ADDITIONS ---
bool isLoading = true; bool isLoading = true;
TextEditingController placeDestinationController = TextEditingController(); TextEditingController placeDestinationController = TextEditingController();
TextEditingController increasFeeFromPassenger = TextEditingController(); TextEditingController increasFeeFromPassenger = TextEditingController();
@@ -280,6 +286,92 @@ class MapPassengerController extends GetxController {
update(); update();
} }
/// Initializes the deep link listener.
/// It checks for the initial link when the app starts and then listens for subsequent links.
Future<void> _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 { void getCurrentLocationFormString() async {
currentLocationToFormPlaces = true; currentLocationToFormPlaces = true;
currentLocationString = 'Waiting for your location'.tr; currentLocationString = 'Waiting for your location'.tr;
@@ -3190,6 +3282,8 @@ class MapPassengerController extends GetxController {
print( print(
"--- MapPassengerController: Closing and cleaning up all resources. ---"); "--- MapPassengerController: Closing and cleaning up all resources. ---");
_linkSubscription?.cancel();
// 1. إلغاء المؤقتات الفردية // 1. إلغاء المؤقتات الفردية
// Using ?.cancel() is safe even if the timer is null // Using ?.cancel() is safe even if the timer is null
markerReloadingTimer.cancel(); markerReloadingTimer.cancel();
@@ -5719,6 +5813,7 @@ class MapPassengerController extends GetxController {
await initilizeGetStorage(); // إعداد سريع await initilizeGetStorage(); // إعداد سريع
await _initMinimalIcons(); // start/end فقط await _initMinimalIcons(); // start/end فقط
await addToken(); // لو لازم للمصادقة await addToken(); // لو لازم للمصادقة
await _initUniLinks();
await getLocation(); // لتحديد الكاميرا await getLocation(); // لتحديد الكاميرا
box.write(BoxName.carType, 'yet'); box.write(BoxName.carType, 'yet');
box.write(BoxName.tipPercentage, '0'); box.write(BoxName.tipPercentage, '0');

View File

@@ -179,6 +179,13 @@ class MyTranslation extends Translations {
"Contacts Loaded": "تم تحميل جهات الاتصال", "Contacts Loaded": "تم تحميل جهات الاتصال",
"Showing": "يتم عرض", "Showing": "يتم عرض",
"of": "من", "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.": "contacts. Others were hidden because they don't have a phone number.":
"جهة اتصال. تم إخفاء البقية لعدم وجود أرقام هواتف لديهم.", "جهة اتصال. تم إخفاء البقية لعدم وجود أرقام هواتف لديهم.",
"No contacts found": "لم يتم العثور على جهات اتصال", "No contacts found": "لم يتم العثور على جهات اتصال",
@@ -1363,6 +1370,8 @@ class MyTranslation extends Translations {
"Edit Your data": "تعديل بياناتك", "Edit Your data": "تعديل بياناتك",
"write vin for your car": "اكتب رقم هيكل سيارتك", "write vin for your car": "اكتب رقم هيكل سيارتك",
"VIN": "رقم الهيكل", "VIN": "رقم الهيكل",
"Device Change Detected": "تم اكتشاف تغيير في الجهاز",
"Please verify your identity": "يرجى التحقق من هويتك",
"write Color for your car": "اكتب لون سيارتك", "write Color for your car": "اكتب لون سيارتك",
"write Make for your car": "اكتب الشركة المصنعة لسيارتك", "write Make for your car": "اكتب الشركة المصنعة لسيارتك",
"write Model for your car": "اكتب موديل سيارتك", "write Model for your car": "اكتب موديل سيارتك",
@@ -1458,6 +1467,19 @@ class MyTranslation extends Translations {
"يرجى البقاء في نقطة الالتقاط المحددة.", "يرجى البقاء في نقطة الالتقاط المحددة.",
"message From Driver": "رسالة من السائق", "message From Driver": "رسالة من السائق",
"Trip is Begin": "بدأت الرحلة", "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": "إلغاء الرحلة من السائق", "Cancel Trip from driver": "إلغاء الرحلة من السائق",
"We will look for a new driver.\nPlease wait.": "We will look for a new driver.\nPlease wait.":
"هنبحث عن سائق جديد.\nمن فضلك انتظر.", "هنبحث عن سائق جديد.\nمن فضلك انتظر.",

View File

@@ -664,154 +664,154 @@ class PaymentController extends GetxController {
Future<void> payWithMTNWallet( Future<void> payWithMTNWallet(
BuildContext context, String amount, String currency) async { BuildContext context, String amount, String currency) async {
// استخدام مؤشر تحميل لتجربة مستخدم أفضل // خزن سياق علوي آمن من البداية
Get.dialog(const Center(child: CircularProgressIndicator()), final BuildContext safeContext =
barrierDismissible: false); Get.overlayContext ?? Get.context ?? context;
// سبينر تحميل
if (!(Get.isDialogOpen ?? false)) {
Get.dialog(const Center(child: CircularProgressIndicator()),
barrierDismissible: false);
}
try { try {
String phone = box.read(BoxName.phoneWallet); final phone = box.read(BoxName.phoneWallet) as String;
String passengerID = box.read(BoxName.passengerID).toString(); final passengerID = box.read(BoxName.passengerID).toString();
String formattedAmount = double.parse(amount).toStringAsFixed(0); final formattedAmount = double.parse(amount).toStringAsFixed(0);
print("🚀 بدء عملية دفع MTN"); print("🚀 بدء عملية دفع MTN");
print( print(
"📦 Payload: passengerID: $passengerID, amount: $formattedAmount, phone: $phone"); "📦 Payload: passengerID: $passengerID, amount: $formattedAmount, phone: $phone");
// التحقق من البصمة (اختياري) // التحقق بالبصمة (اختياري) + حماية من الـ await
bool isAuthSupported = await LocalAuthentication().isDeviceSupported(); final localAuth = LocalAuthentication();
final isAuthSupported = await localAuth.isDeviceSupported();
if (isAuthSupported) { if (isAuthSupported) {
bool didAuthenticate = await LocalAuthentication().authenticate( final didAuth = await localAuth.authenticate(
localizedReason: 'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع', localizedReason: 'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع',
); );
if (!didAuthenticate) { if (!didAuth) {
if (Get.isDialogOpen ?? false) Get.back(); if (Get.isDialogOpen == true) Get.back();
print("❌ المستخدم لم يؤكد بالبصمة/الوجه"); print("❌ المستخدم لم يؤكد بالبصمة/الوجه");
return; return;
} }
} }
// 1️⃣ استدعاء mtn_start_payment.php (الملف الجديد) // 1) بدء الدفع
var responseData = await CRUD().postWallet( final responseData = await CRUD().postWalletMtn(
link: AppLink.payWithMTNStart, link: AppLink.payWithMTNStart,
payload: { payload: {
"amount": formattedAmount, "amount": formattedAmount,
"passengerId": passengerID, "passengerId": passengerID,
"phone": phone, "phone": phone,
"lang": box.read(BoxName.lang) ?? 'ar',
}, },
); );
print("✅ استجابة الخادم (mtn_start_payment.php):"); // print("✅ استجابة الخادم (mtn_start_payment.php):");
print(responseData); // print(responseData);
Log.print('responseData: ${responseData}');
// --- بداية التعديل المهم ---
// التحقق القوي من الاستجابة لتجنب الأخطاء
Map<String, dynamic> startRes;
// فحص الاستجابة بقوة
late final Map<String, dynamic> startRes;
if (responseData is Map<String, dynamic>) { if (responseData is Map<String, dynamic>) {
// إذا كانت الاستجابة بالفعل Map، استخدمها مباشرة
startRes = responseData; startRes = responseData;
} else if (responseData is String) { } else if (responseData is String) {
// إذا كانت نص، حاول تحليلها كـ JSON startRes = json.decode(responseData) as Map<String, dynamic>;
try {
startRes = json.decode(responseData);
} catch (e) {
throw Exception(
"فشل في تحليل استجابة الخادم. الاستجابة: $responseData");
}
} else { } else {
// نوع غير متوقع
throw Exception("تم استلام نوع بيانات غير متوقع من الخادم."); throw Exception("تم استلام نوع بيانات غير متوقع من الخادم.");
} }
if (startRes['status'] != 'success') { if (startRes['status'] != 'success') {
String errorMsg = startRes['message']?.toString() ?? final errorMsg = startRes['message']['Error']?.toString().tr ??
"فشل بدء عملية الدفع. حاول مرة أخرى."; "فشل بدء عملية الدفع. حاول مرة أخرى.";
throw Exception(errorMsg); throw Exception(errorMsg);
} }
// --- نهاية التعديل المهم ---
// استخراج البيانات بأمان final messageData = startRes["message"] as Map<String, dynamic>;
final messageData = startRes["message"];
final invoiceNumber = messageData["invoiceNumber"].toString(); final invoiceNumber = messageData["invoiceNumber"].toString();
final operationNumber = messageData["operationNumber"].toString(); final operationNumber = messageData["operationNumber"].toString();
final guid = messageData["guid"].toString(); final guid = messageData["guid"].toString();
print( // print(
"📄 invoiceNumber: $invoiceNumber, 🔢 operationNumber: $operationNumber, 🧭 guid: $guid"); // "📄 invoiceNumber: $invoiceNumber, 🔢 operationNumber: $operationNumber, 🧭 guid: $guid");
if (Get.isDialogOpen ?? false) // أغلق السبينر قبل إظهار حوار OTP
Get.back(); // إغلاق مؤشر التحميل قبل عرض حوار OTP if (Get.isDialogOpen == true) Get.back();
// 2️⃣ عرض واجهة إدخال OTP // 2) إدخال OTP بـ Get.defaultDialog (لا يستخدم context قابل للتلف)
String? otp = await showDialog<String>( String otpInput = "";
context: context, await Get.defaultDialog(
builder: (context) { title: "أدخل كود التحقق",
String input = ""; barrierDismissible: false,
return AlertDialog( content: TextField(
title: const Text("أدخل كود التحقق"), keyboardType: TextInputType.number,
content: TextField( decoration: const InputDecoration(hintText: "كود OTP"),
keyboardType: TextInputType.number, onChanged: (v) => otpInput = v,
decoration: const InputDecoration(hintText: "كود OTP"), ),
onChanged: (val) => input = val, confirm: TextButton(
), onPressed: () {
actions: [ if (otpInput.isEmpty ||
TextButton( otpInput.length < 4 ||
child: const Text("تأكيد"), otpInput.length > 8) {
onPressed: () => Navigator.of(context).pop(input), Get.snackbar("تنبيه", "أدخل كود OTP صحيح (48 أرقام)");
), return;
TextButton( }
child: const Text("إلغاء"), Get.back(result: otpInput);
onPressed: () => Navigator.of(context).pop(), },
), 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"); print("❌ لم يتم إدخال OTP");
return; return;
} }
print("🔐 تم إدخال OTP: $otp"); print("🔐 تم إدخال OTP: $otpInput");
// سبينر أثناء التأكيد
Get.dialog(const Center(child: CircularProgressIndicator()), Get.dialog(const Center(child: CircularProgressIndicator()),
barrierDismissible: false); barrierDismissible: false);
// 3️⃣ استدعاء mtn_confirm.php // 3) تأكيد الدفع
var confirmRes = await CRUD().postWallet( final confirmRes = await CRUD().postWalletMtn(
link: AppLink.payWithMTNConfirm, link: AppLink.payWithMTNConfirm,
payload: { payload: {
"invoiceNumber": invoiceNumber, "invoiceNumber": invoiceNumber,
"operationNumber": operationNumber, "operationNumber": operationNumber,
"guid": guid, "guid": guid,
"otp": otp, "otp": otpInput,
"phone": phone, "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("✅ استجابة mtn_confirm.php:");
print(confirmRes); // Log.print('confirmRes: ${confirmRes}');
if (confirmRes != null && confirmRes['status'] == 'success') { final ok = (confirmRes is Map && confirmRes['status'] == 'success');
if (ok) {
Get.defaultDialog( Get.defaultDialog(
title: "✅ نجاح", title: "✅ نجاح",
content: const Text("تمت عملية الدفع وإضافة الرصيد إلى محفظتك."), content: const Text("تمت عملية الدفع وإضافة الرصيد إلى محفظتك."),
); );
await getPassengerWallet();
} else { } else {
String errorMsg = final errorMsg = (confirmRes['message']['message']?.toString()) ??
confirmRes?['message']?.toString() ?? "فشل في تأكيد الدفع"; "فشل في تأكيد الدفع";
Get.defaultDialog( Get.defaultDialog(title: "❌ فشل", content: Text(errorMsg.tr));
title: "❌ فشل",
content: Text(errorMsg),
);
} }
} catch (e, s) { } catch (e, s) {
print("🔥 خطأ أثناء الدفع عبر MTN:"); print("🔥 خطأ أثناء الدفع عبر MTN:");
print(e); print(e);
print(s); print(s);
if (Get.isDialogOpen ?? false) Get.back(); if (Get.isDialogOpen == true) Get.back();
Get.defaultDialog( Get.defaultDialog(
title: 'حدث خطأ', title: 'حدث خطأ',
content: Text(e.toString().replaceFirst("Exception: ", "")), content: Text(e.toString().replaceFirst("Exception: ", "")),

View File

@@ -1,6 +1,8 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:Intaleq/controller/functions/crud.dart';
import 'package:Intaleq/controller/payment/paymob/paymob_response.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/contact_us.dart';
import 'package:Intaleq/views/home/HomePage/share_app_page.dart'; import 'package:Intaleq/views/home/HomePage/share_app_page.dart';
@@ -133,7 +135,30 @@ void main() async {
), ),
]); ]);
runApp(const MyApp()); runZonedGuarded<Future<void>>(() 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 { class MyApp extends StatelessWidget {

View File

@@ -9,6 +9,8 @@ import 'package:Intaleq/controller/functions/toast.dart';
import 'package:Intaleq/controller/payment/payment_controller.dart'; import 'package:Intaleq/controller/payment/payment_controller.dart';
import '../../../main.dart'; import '../../../main.dart';
import '../../widgets/elevated_btn.dart';
import '../../widgets/my_textField.dart';
class PassengerWalletDialog extends StatelessWidget { class PassengerWalletDialog extends StatelessWidget {
const PassengerWalletDialog({ const PassengerWalletDialog({
@@ -264,76 +266,143 @@ void showPaymentOptions(BuildContext context, PaymentController controller) {
}, },
) )
: const SizedBox(), : const SizedBox(),
box.read(BoxName.phoneWallet) != null // box.read(BoxName.phoneWallet) != null
? CupertinoActionSheetAction( // ? CupertinoActionSheetAction(
child: Text('💰 Pay with Wallet'.tr), // 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<FormState>();
// 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 { onPressed: () async {
if (controller.selectedAmount != 0) { if (controller.formKey.currentState!.validate()) {
controller.isLoading = true; if (controller.selectedAmount != 0) {
controller.update(); controller.isLoading = true;
controller.payWithMTNWallet( controller.update();
context, box.write(BoxName.phoneWallet,
controller.selectedAmount.toString(), (controller.walletphoneController.text));
'SYP', Get.back();
); await controller.payWithMTNWallet(
await controller.getPassengerWallet(); context,
controller.isLoading = false; controller.selectedAmount.toString(),
controller.update(); 'SYP',
} else { );
Toast.show(context, '⚠️ You need to choose an amount!'.tr, await controller.getPassengerWallet();
AppColor.redColor);
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( cancelButton: CupertinoActionSheetAction(
child: Text('Cancel'.tr), child: Text('Cancel'.tr),

View File

@@ -233,6 +233,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.19.1" 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: convert:
dependency: transitive dependency: transitive
description: description:
@@ -1112,6 +1128,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.1+1" 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: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1336,6 +1360,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
nm:
dependency: transitive
description:
name: nm
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
octo_image: octo_image:
dependency: transitive dependency: transitive
description: description:
@@ -1908,6 +1940,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" 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: url_launcher:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@@ -59,7 +59,7 @@ dependencies:
sign_in_with_apple: ^6.1.0 sign_in_with_apple: ^6.1.0
firebase_auth: ^5.1.2 firebase_auth: ^5.1.2
device_info_plus: ^11.3.0 device_info_plus: ^11.3.0
# uni_links: ^0.5.1 uni_links: ^0.5.1
googleapis_auth: ^1.6.0 googleapis_auth: ^1.6.0
flutter_confetti: ^0.3.0 flutter_confetti: ^0.3.0
# intl_phone_field: ^3.1.0 # intl_phone_field: ^3.1.0
@@ -75,6 +75,8 @@ dependencies:
shimmer: ^3.0.0 shimmer: ^3.0.0
share_plus: ^11.0.0 share_plus: ^11.0.0
asn1lib: ^1.6.5 asn1lib: ^1.6.5
internet_connection_checker: ^3.0.1
connectivity_plus: ^6.1.5
# home_widget: ^0.7.0+1 # home_widget: ^0.7.0+1
dev_dependencies: dev_dependencies: