Initial push to my private server
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
48
lib/controller/functions/network/connection_check.dart
Normal file
48
lib/controller/functions/network/connection_check.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
48
lib/controller/functions/network/net_guard.dart
Normal file
48
lib/controller/functions/network/net_guard.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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من فضلك انتظر.",
|
||||
|
||||
@@ -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 صحيح (4–8 أرقام)");
|
||||
return;
|
||||
}
|
||||
Get.back(result: otpInput);
|
||||
},
|
||||
child: const Text("تأكيد"),
|
||||
),
|
||||
cancel: TextButton(
|
||||
onPressed: () => Get.back(result: null),
|
||||
child: const Text("إلغاء"),
|
||||
),
|
||||
).then((res) => otpInput = (res ?? "") as String);
|
||||
|
||||
if (otp == null || otp.isEmpty) {
|
||||
if (otpInput.isEmpty) {
|
||||
print("❌ لم يتم إدخال OTP");
|
||||
return;
|
||||
}
|
||||
print("🔐 تم إدخال OTP: $otp");
|
||||
print("🔐 تم إدخال OTP: $otpInput");
|
||||
|
||||
// سبينر أثناء التأكيد
|
||||
Get.dialog(const Center(child: CircularProgressIndicator()),
|
||||
barrierDismissible: false);
|
||||
|
||||
// 3️⃣ استدعاء mtn_confirm.php
|
||||
var confirmRes = await CRUD().postWallet(
|
||||
// 3) تأكيد الدفع
|
||||
final confirmRes = await CRUD().postWalletMtn(
|
||||
link: AppLink.payWithMTNConfirm,
|
||||
payload: {
|
||||
"invoiceNumber": invoiceNumber,
|
||||
"operationNumber": operationNumber,
|
||||
"guid": guid,
|
||||
"otp": otp,
|
||||
"otp": otpInput,
|
||||
"phone": phone,
|
||||
"lang": box.read(BoxName.lang) ?? 'ar',
|
||||
},
|
||||
);
|
||||
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
if (Get.isDialogOpen == true) Get.back();
|
||||
|
||||
print("✅ استجابة mtn_confirm.php:");
|
||||
print(confirmRes);
|
||||
// print("✅ استجابة mtn_confirm.php:");
|
||||
// Log.print('confirmRes: ${confirmRes}');
|
||||
|
||||
if (confirmRes != null && confirmRes['status'] == 'success') {
|
||||
final ok = (confirmRes is Map && confirmRes['status'] == 'success');
|
||||
if (ok) {
|
||||
Get.defaultDialog(
|
||||
title: "✅ نجاح",
|
||||
content: const Text("تمت عملية الدفع وإضافة الرصيد إلى محفظتك."),
|
||||
);
|
||||
await getPassengerWallet();
|
||||
} else {
|
||||
String errorMsg =
|
||||
confirmRes?['message']?.toString() ?? "فشل في تأكيد الدفع";
|
||||
Get.defaultDialog(
|
||||
title: "❌ فشل",
|
||||
content: Text(errorMsg),
|
||||
);
|
||||
final errorMsg = (confirmRes['message']['message']?.toString()) ??
|
||||
"فشل في تأكيد الدفع";
|
||||
Get.defaultDialog(title: "❌ فشل", content: Text(errorMsg.tr));
|
||||
}
|
||||
} catch (e, s) {
|
||||
print("🔥 خطأ أثناء الدفع عبر MTN:");
|
||||
print(e);
|
||||
print(s);
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
if (Get.isDialogOpen == true) Get.back();
|
||||
Get.defaultDialog(
|
||||
title: 'حدث خطأ',
|
||||
content: Text(e.toString().replaceFirst("Exception: ", "")),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user