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.
minSdk = 29
targetSdk = 36
versionCode = 11
versionName = '1.0.11'
versionCode = 13
versionName = '1.0.13'
multiDexEnabled = true
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"

View File

@@ -43,15 +43,15 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</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" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Accepts URIs that begin with "sefer://" -->
<data android:scheme="sefer" />
<!-- Accepts URIs that begin with "https://sefer.live" -->
<data android:scheme="https" android:host="sefer.live" />
<!-- Accepts the custom scheme 'intaleq' and host 'map' -->
<data android:scheme="intaleq" android:host="map" />
</intent-filter>
</activity>

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -18,7 +18,7 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.11.0' apply false
id "com.android.application" version '8.11.1' apply false
// START: FlutterFire Configuration
id "com.google.gms.google-services" version "4.3.10" apply false
// END: FlutterFire Configuration

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

View File

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

View File

@@ -1,9 +1,25 @@
<?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">
<plist version="1.0">
<dict>
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<!-- استبدل هذا بمعرّف الحزمة الخاص بك -->
<string>com.intaleq.app</string>
<key>CFBundleURLSchemes</key>
<array>
<string>intaleq</string>
</array>
</dict>
</array>
<key>FlutterDeepLinkingEnabled</key>
<true />
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<true />
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@@ -19,7 +35,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>6</string>
<string>7</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -29,13 +45,14 @@
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>com.googleusercontent.apps.1086900987150-9jv4oa8l3t23d54lrf27c1d22tbt9i6d</string>
<string>
com.googleusercontent.apps.1086900987150-9jv4oa8l3t23d54lrf27c1d22tbt9i6d</string>
</array>
</dict>
<dict/>
<dict />
</array>
<key>CFBundleVersion</key>
<string>1.0.6</string>
<string>1.0.7</string>
<key>FirebaseAppDelegateProxyEnabled</key>
<string>NO</string>
<key>GMSApiKey</key>
@@ -46,7 +63,7 @@
<string>comgooglemaps</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<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>
@@ -70,7 +87,7 @@
<key>NSPhotoLibraryUsageDescription</key>
<string>This app requires access to the photo library to upload pictures.</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<true />
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
@@ -95,6 +112,6 @@
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
<false />
</dict>
</plist>

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
String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
if (JwtDecoder.isExpired(token)) {
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';
}
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();
final hmac = box.read(BoxName.hmac);
// Log.print('hmac: ${hmac}');
var url = Uri.parse(
link,
);
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';
}
}
Future<dynamic> post(
{required String link, Map<String, dynamic>? payload}) async {
var url = Uri.parse(link);
try {
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]}'
},
);
// Log.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'];
}
} 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';
}
} 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();
var jwt = await LoginController().getJwtWallet();
final hmac = box.read(BoxName.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,
);
if (result is Map && result['status'] == 'success') {
return jsonEncode(result);
}
return result;
}
// =======================================================================
// 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 {
// 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 $s',
'X-HMAC-Auth': hmac.toString(),
"Authorization": "Bearer $s",
"X-HMAC-Auth": hmac.toString(),
},
);
// 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
try {
final jsonData = jsonDecode(response.body);
if (jsonData is Map && jsonData['error'] == 'Token expired') {
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 {
'status': 'failure',
'message': 'token_expired',
'code': 401
};
}
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 {
// استخدام مؤشر تحميل لتجربة مستخدم أفضل
// خزن سياق علوي آمن من البداية
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("أدخل كود التحقق"),
// 2) إدخال OTP بـ Get.defaultDialog (لا يستخدم context قابل للتلف)
String otpInput = "";
await Get.defaultDialog(
title: "أدخل كود التحقق",
barrierDismissible: false,
content: TextField(
keyboardType: TextInputType.number,
decoration: const InputDecoration(hintText: "كود OTP"),
onChanged: (val) => input = val,
onChanged: (v) => otpInput = v,
),
actions: [
TextButton(
child: const Text("تأكيد"),
onPressed: () => Navigator.of(context).pop(input),
),
TextButton(
child: const Text("إلغاء"),
onPressed: () => Navigator.of(context).pop(),
),
],
);
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 {
),
]);
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.formKey.currentState!.validate()) {
if (controller.selectedAmount != 0) {
controller.isLoading = true;
controller.update();
controller.payWithMTNWallet(
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);
'⚠️ You need to choose an amount!'.tr,
AppColor.redColor,
);
}
}
},
),
],
),
barrierDismissible: false,
);
},
child: Image.asset(
'assets/images/mtn.png',
width: 70,
height: 70,
fit: BoxFit.contain,
),
)
],
cancelButton: CupertinoActionSheetAction(
child: Text('Cancel'.tr),

View File

@@ -233,6 +233,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
connectivity_plus:
dependency: "direct main"
description:
name: connectivity_plus
sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec
url: "https://pub.dev"
source: hosted
version: "6.1.5"
connectivity_plus_platform_interface:
dependency: transitive
description:
name: connectivity_plus_platform_interface
sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
convert:
dependency: transitive
description:
@@ -1112,6 +1128,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
internet_connection_checker:
dependency: "direct main"
description:
name: internet_connection_checker
sha256: ee08f13d8b13b978affe226e9274ca3ba7a9bed07c9479e8ae245f785b7a488a
url: "https://pub.dev"
source: hosted
version: "3.0.1"
intl:
dependency: "direct main"
description:
@@ -1336,6 +1360,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
nm:
dependency: transitive
description:
name: nm
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
octo_image:
dependency: transitive
description:
@@ -1908,6 +1940,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uni_links:
dependency: "direct main"
description:
name: uni_links
sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
uni_links_platform_interface:
dependency: transitive
description:
name: uni_links_platform_interface
sha256: "929cf1a71b59e3b7c2d8a2605a9cf7e0b125b13bc858e55083d88c62722d4507"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
uni_links_web:
dependency: transitive
description:
name: uni_links_web
sha256: "7539db908e25f67de2438e33cc1020b30ab94e66720b5677ba6763b25f6394df"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
url_launcher:
dependency: "direct main"
description:

View File

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