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

@@ -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,

View File

@@ -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<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({
required String link,
Map<String, dynamic>? 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<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({
required String link,
Map<String, dynamic>? 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<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 {
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<String, dynamic> 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<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 {
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<String, dynamic>? 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.
}

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: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<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 {
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');

View File

@@ -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من فضلك انتظر.",

View File

@@ -664,154 +664,154 @@ class PaymentController extends GetxController {
Future<void> 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<String, dynamic> startRes;
// print("✅ استجابة الخادم (mtn_start_payment.php):");
// print(responseData);
Log.print('responseData: ${responseData}');
// فحص الاستجابة بقوة
late final Map<String, dynamic> startRes;
if (responseData is Map<String, dynamic>) {
// إذا كانت الاستجابة بالفعل 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<String, dynamic>;
} 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<String, dynamic>;
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<String>(
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 صحيح (48 أرقام)");
return;
}
Get.back(result: otpInput);
},
child: const Text("تأكيد"),
),
cancel: TextButton(
onPressed: () => Get.back(result: null),
child: const Text("إلغاء"),
),
).then((res) => otpInput = (res ?? "") as String);
if (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: ", "")),

View File

@@ -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<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 {

View File

@@ -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<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 {
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),