25-09-22/1
This commit is contained in:
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"java.configuration.updateBuildConfiguration": "disabled"
|
||||
}
|
||||
@@ -27,7 +27,7 @@ android {
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path "src/main/cpp/CMakeLists.txt"
|
||||
version "3.31.5"
|
||||
version "3.22.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ android {
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = 29
|
||||
targetSdk = 36
|
||||
versionCode = 13
|
||||
versionName = '1.0.13'
|
||||
versionCode = 14
|
||||
versionName = '1.0.14'
|
||||
multiDexEnabled = true
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
|
||||
|
||||
@@ -45,14 +45,18 @@
|
||||
</intent-filter>
|
||||
|
||||
|
||||
<!-- Deep Linking Intent Filter -->
|
||||
<!-- ... other intent-filters ... -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<!-- Accepts the custom scheme 'intaleq' and host 'map' -->
|
||||
<data android:scheme="intaleq" android:host="map" />
|
||||
<!--
|
||||
هنا تضع الرابط المخصص لتطبيقك.
|
||||
مثال: عند الضغط على رابط `intaleq://route?lat=...` سيتم فتح التطبيق.
|
||||
-->
|
||||
<data android:scheme="intaleq" android:host="route" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<!-- Existing meta-data and other activities remain unchanged -->
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,3 +6,4 @@ android.nonTransitiveRClass=true
|
||||
android.nonFinalResIds=true
|
||||
dart.obfuscation=true
|
||||
android.enableR8.fullMode=true
|
||||
org.gradle.java.home=/Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home
|
||||
|
||||
BIN
assets/images/cashMTN.png
Normal file
BIN
assets/images/cashMTN.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
BIN
assets/images/syriatel.png
Normal file
BIN
assets/images/syriatel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
@@ -133,6 +133,10 @@ class AppLink {
|
||||
"$seferPaymentServer/ride/mtn/passenger/mtn_confirm.php";
|
||||
static String payWithMTNStart =
|
||||
"$seferPaymentServer/ride/mtn/passenger/mtn_start.php";
|
||||
static String payWithSyriatelConfirm =
|
||||
"$seferPaymentServer/ride/syriatel/passenger/confirm_payment.php";
|
||||
static String payWithSyriatelStart =
|
||||
"$seferPaymentServer/ride/syriatel/passenger/start_payment.php";
|
||||
static String getDriverpaymentToday =
|
||||
"$seferPaymentServer/ride/payment/get.php";
|
||||
static String getCountRide =
|
||||
|
||||
@@ -182,13 +182,13 @@ class LoginController extends GetxController {
|
||||
loginUsingCredentials(String passengerID, email) async {
|
||||
isloading = true;
|
||||
update();
|
||||
bool isTokenExpired = JwtDecoder.isExpired(
|
||||
r(box.read(BoxName.jwt)).toString().split(AppInformation.addd)[0]);
|
||||
// bool isTokenExpired = JwtDecoder.isExpired(
|
||||
// r(box.read(BoxName.jwt)).toString().split(AppInformation.addd)[0]);
|
||||
|
||||
if (isTokenExpired) {
|
||||
// Log.print('isTokenExpired loginUsingCredentials: ${isTokenExpired}');
|
||||
await getJWT();
|
||||
}
|
||||
// if (isTokenExpired) {
|
||||
// // Log.print('isTokenExpired loginUsingCredentials: ${isTokenExpired}');
|
||||
// await getJWT();
|
||||
// }
|
||||
|
||||
var res =
|
||||
await CRUD().get(link: AppLink.loginFromGooglePassenger, payload: {
|
||||
|
||||
@@ -94,6 +94,14 @@ class OtpVerificationController extends GetxController {
|
||||
[],
|
||||
'cancel.wav',
|
||||
);
|
||||
CRUD().post(
|
||||
link:
|
||||
'${AppLink.seferPaymentServer}/auth/token/update_passenger_token.php',
|
||||
payload: {
|
||||
'token': box.read(BoxName.tokenDriver).toString(),
|
||||
'fingerPrint': finger.toString(),
|
||||
'passengerID': box.read(BoxName.passengerID).toString(),
|
||||
});
|
||||
Get.offAll(() => const MapPagePassenger());
|
||||
} else {
|
||||
Get.snackbar('Verification Failed', 'OTP is incorrect or expired');
|
||||
|
||||
@@ -99,8 +99,8 @@ class CRUD {
|
||||
return jsonData;
|
||||
} else {
|
||||
// Log API logical errors (e.g., "Customer not found")
|
||||
if (response.body == 'failure') {
|
||||
return 'failure';
|
||||
if (jsonData['status'] == 'failure') {
|
||||
// return 'failure';
|
||||
} else {
|
||||
addError(
|
||||
'API Logic Error: ${jsonData['status']}',
|
||||
@@ -122,6 +122,8 @@ class CRUD {
|
||||
} else if (response.statusCode == 401) {
|
||||
var jsonData = jsonDecode(response.body);
|
||||
if (jsonData['error'] == 'Token expired') {
|
||||
await Get.put(LoginController()).getJWT();
|
||||
// mySnackbarSuccess('please order now'.tr);
|
||||
return 'token_expired';
|
||||
} else {
|
||||
addError(
|
||||
@@ -161,10 +163,10 @@ class CRUD {
|
||||
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];
|
||||
}
|
||||
// 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",
|
||||
@@ -185,10 +187,10 @@ class CRUD {
|
||||
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];
|
||||
}
|
||||
// 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",
|
||||
|
||||
63
lib/controller/home/deep_link_controller.dart
Normal file
63
lib/controller/home/deep_link_controller.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
import 'dart:async';
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
|
||||
class DeepLinkController extends GetxController {
|
||||
// استخدم AppLinks للتعامل مع الروابط
|
||||
final _appLinks = AppLinks();
|
||||
StreamSubscription<Uri>? _linkSubscription;
|
||||
|
||||
// متغير لتخزين الإحداثيات القادمة من الرابط
|
||||
final Rx<LatLng?> deepLinkLatLng = Rx<LatLng?>(null);
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// ابدأ بالاستماع للروابط عند تشغيل التطبيق
|
||||
initDeepLinks();
|
||||
}
|
||||
|
||||
Future<void> initDeepLinks() async {
|
||||
// الاستماع إلى الروابط القادمة
|
||||
_linkSubscription = _appLinks.uriLinkStream.listen((uri) {
|
||||
print('Received deep link: $uri');
|
||||
_handleLink(uri);
|
||||
});
|
||||
|
||||
// جلب الرابط الأولي الذي قد يكون فتح التطبيق
|
||||
final initialUri = await _appLinks.getInitialLink();
|
||||
if (initialUri != null) {
|
||||
print('Received initial deep link: $initialUri');
|
||||
_handleLink(initialUri);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleLink(Uri uri) {
|
||||
// تحقق من أن الرابط يتبع النمط المتوقع (مثال: intaleq://route?lat=xx&lng=yy)
|
||||
if (uri.scheme == 'intaleq' && uri.host == 'route') {
|
||||
// استخراج خطوط الطول والعرض من الرابط
|
||||
final latString = uri.queryParameters['lat'];
|
||||
final lngString = uri.queryParameters['lng'];
|
||||
|
||||
if (latString != null && lngString != null) {
|
||||
final double? lat = double.tryParse(latString);
|
||||
final double? lng = double.tryParse(lngString);
|
||||
|
||||
if (lat != null && lng != null) {
|
||||
// إذا كانت الإحداثيات صالحة، قم بتحديث المتغير
|
||||
// ستستمع وحدة التحكم في الخريطة لهذا التغيير
|
||||
deepLinkLatLng.value = LatLng(lat, lng);
|
||||
print('Parsed LatLng from deep link: ${deepLinkLatLng.value}');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
// تأكد من إلغاء الاشتراك عند إغلاق وحدة التحكم
|
||||
_linkSubscription?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ 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';
|
||||
@@ -14,7 +13,6 @@ 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';
|
||||
@@ -54,14 +52,11 @@ import '../functions/launch.dart';
|
||||
import '../functions/package_info.dart';
|
||||
import '../functions/secure_storage.dart';
|
||||
import '../payment/payment_controller.dart';
|
||||
import 'deep_link_controller.dart';
|
||||
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();
|
||||
@@ -276,6 +271,11 @@ class MapPassengerController extends GetxController {
|
||||
late DateTime newTime = DateTime.now();
|
||||
int hours = 0;
|
||||
int minutes = 0;
|
||||
|
||||
// --- إضافة جديدة: للوصول إلى وحدة التحكم بالروابط ---
|
||||
final DeepLinkController _deepLinkController = Get.find();
|
||||
// ------------------------------------------------
|
||||
|
||||
void onChangedPassengerCount(int newValue) {
|
||||
selectedPassengerCount = newValue;
|
||||
update();
|
||||
@@ -286,92 +286,6 @@ 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;
|
||||
@@ -3282,8 +3196,6 @@ 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();
|
||||
@@ -3616,6 +3528,8 @@ class MapPassengerController extends GetxController {
|
||||
update();
|
||||
}
|
||||
|
||||
/// تحويل نصف قطر بالكيلومتر إلى دلتا درجات عرض
|
||||
|
||||
double _haversineKm(double lat1, double lon1, double lat2, double lon2) {
|
||||
const R = 6371.0; // km
|
||||
final dLat = (lat2 - lat1) * math.pi / 180.0;
|
||||
@@ -3629,6 +3543,29 @@ class MapPassengerController extends GetxController {
|
||||
return R * c;
|
||||
}
|
||||
|
||||
/// تحويل نصف قطر بالكيلومتر إلى دلتا درجات عرض
|
||||
double _kmToLatDelta(double km) => km / 111.0;
|
||||
|
||||
/// تحويل نصف قطر بالكيلومتر إلى دلتا درجات طول (تعتمد على خط العرض)
|
||||
double _kmToLngDelta(double km, double atLat) =>
|
||||
km / (111.320 * math.cos(atLat * math.pi / 180.0)).abs().clamp(1e-6, 1e9);
|
||||
|
||||
/// حساب درجة التطابق النصي (كل كلمة تبدأ بها الاسم = 2 نقاط، يحتويها = 1 نقطة)
|
||||
double _relevanceScore(String name, String query) {
|
||||
final n = name.toLowerCase();
|
||||
final parts =
|
||||
query.toLowerCase().split(RegExp(r'\s+')).where((p) => p.length >= 2);
|
||||
double s = 0.0;
|
||||
for (final p in parts) {
|
||||
if (n.startsWith(p)) {
|
||||
s += 2.0;
|
||||
} else if (n.contains(p)) {
|
||||
s += 1.0;
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Future<void> getPlaces() async {
|
||||
final q = placeDestinationController.text.trim();
|
||||
if (q.isEmpty) {
|
||||
@@ -3640,11 +3577,17 @@ class MapPassengerController extends GetxController {
|
||||
final lat = passengerLocation.latitude;
|
||||
final lng = passengerLocation.longitude;
|
||||
|
||||
const range = 2.2;
|
||||
final latMin = lat - range;
|
||||
final latMax = lat + range;
|
||||
final lngMin = lng - range;
|
||||
final lngMax = lng + range;
|
||||
// نصف قطر البحث بالكيلومتر (عدّل حسب رغبتك)
|
||||
const radiusKm = 200.0;
|
||||
|
||||
// حساب الباوند الصحيح (درجات، وليس 2.2 درجة ثابتة)
|
||||
final latDelta = _kmToLatDelta(radiusKm);
|
||||
final lngDelta = _kmToLngDelta(radiusKm, lat);
|
||||
|
||||
final latMin = lat - latDelta;
|
||||
final latMax = lat + latDelta;
|
||||
final lngMin = lng - lngDelta;
|
||||
final lngMax = lng + lngDelta;
|
||||
|
||||
try {
|
||||
final response = await CRUD().post(
|
||||
@@ -3658,43 +3601,59 @@ class MapPassengerController extends GetxController {
|
||||
},
|
||||
);
|
||||
|
||||
if (response != 'failure') {
|
||||
final list = (response['message'] as List?) ?? [];
|
||||
// يدعم شكلي استجابة: إما {"...","message":[...]} أو قائمة مباشرة [...]
|
||||
List list;
|
||||
if (response is Map && response['message'] is List) {
|
||||
list = List.from(response['message'] as List);
|
||||
} else if (response is List) {
|
||||
list = List.from(response);
|
||||
} else {
|
||||
print('Unexpected response shape');
|
||||
return;
|
||||
}
|
||||
|
||||
// احسب المسافة وألصقها بكل عنصر
|
||||
// جهّز الحقول المحتملة للأسماء
|
||||
String _bestName(Map p) {
|
||||
return (p['name'] ?? p['name_ar'] ?? p['name_en'] ?? '').toString();
|
||||
}
|
||||
|
||||
// احسب المسافة ودرجة التطابق والنقاط
|
||||
for (final p in list) {
|
||||
final plat = double.tryParse(p['latitude']?.toString() ?? '') ?? 0.0;
|
||||
final plng = double.tryParse(p['longitude']?.toString() ?? '') ?? 0.0;
|
||||
p['distanceKm'] = _haversineKm(lat, lng, plat, plng);
|
||||
|
||||
final d = _haversineKm(lat, lng, plat, plng);
|
||||
final rel = _relevanceScore(_bestName(p), q);
|
||||
|
||||
// معادلة ترتيب ذكية: مسافة أقل + تطابق أعلى = نقاط أعلى
|
||||
// تضيف +1 لضمان عدم وصول الوزن للصفر عند عدم وجود تطابق
|
||||
final score = (1.0 / (1.0 + d)) * (1.0 + rel);
|
||||
|
||||
p['distanceKm'] = d;
|
||||
p['relevance'] = rel;
|
||||
p['score'] = score;
|
||||
}
|
||||
|
||||
// رتّب حسب الأقرب
|
||||
// رتّب حسب score تنازليًا، ثم المسافة تصاعديًا كحسم
|
||||
list.sort((a, b) {
|
||||
final sa = (a['score'] ?? 0.0) as double;
|
||||
final sb = (b['score'] ?? 0.0) as double;
|
||||
final cmp = sb.compareTo(sa);
|
||||
if (cmp != 0) return cmp;
|
||||
final da = (a['distanceKm'] ?? 1e9) as double;
|
||||
final db = (b['distanceKm'] ?? 1e9) as double;
|
||||
final cmp = da.compareTo(db);
|
||||
if (cmp != 0) return cmp;
|
||||
|
||||
// تعادل؟ فضّل من يطابق الاسم
|
||||
final nameA =
|
||||
(a['name'] ?? a['name_ar'] ?? a['name_en'] ?? '').toString();
|
||||
final nameB =
|
||||
(b['name'] ?? b['name_ar'] ?? b['name_en'] ?? '').toString();
|
||||
final qLower = q.toLowerCase();
|
||||
final hitA = nameA.toLowerCase().contains(qLower) ? 0 : 1;
|
||||
final hitB = nameB.toLowerCase().contains(qLower) ? 0 : 1;
|
||||
return hitA.compareTo(hitB);
|
||||
return da.compareTo(db);
|
||||
});
|
||||
|
||||
placesDestination = list;
|
||||
// خذ أول 10–15 للعرض (اختياري)، أو اعرض الكل
|
||||
placesDestination = list.take(15).toList();
|
||||
Log.print('placesDestination: $placesDestination');
|
||||
update();
|
||||
} else {
|
||||
print('Server error');
|
||||
}
|
||||
} catch (e) {
|
||||
print('Exception: $e');
|
||||
print('Exception in getPlaces: $e');
|
||||
}
|
||||
} // Future getPlaces() async {
|
||||
}
|
||||
|
||||
// var languageCode;
|
||||
|
||||
// // تحديد اللغة حسب الإدخال
|
||||
@@ -5804,16 +5763,45 @@ class MapPassengerController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
// --- دالة جديدة للاستماع ومعالجة الرابط ---
|
||||
void _listenForDeepLink() {
|
||||
// استمع إلى أي تغيير في الإحداثيات القادمة من الرابط
|
||||
ever(_deepLinkController.deepLinkLatLng, (LatLng? latLng) {
|
||||
if (latLng != null) {
|
||||
print('MapPassengerController detected deep link LatLng: $latLng');
|
||||
// عندما يتم استلام إحداثيات جديدة، عينها كوجهة
|
||||
myDestination = latLng;
|
||||
|
||||
// قم بتشغيل المنطق الخاص بك لعرض المسار
|
||||
// (تأكد من أن `passengerLocation` لديها قيمة أولاً)
|
||||
if (passengerLocation != null) {
|
||||
getDirectionMap(
|
||||
'${passengerLocation.latitude},${passengerLocation.longitude}',
|
||||
'${myDestination.latitude},${myDestination.longitude}');
|
||||
} else {
|
||||
// يمكنك إظهار رسالة للمستخدم لتمكين الموقع أولاً
|
||||
print(
|
||||
'Cannot process deep link route yet, passenger location is null.');
|
||||
}
|
||||
|
||||
// إعادة تعيين القيمة إلى null لمنع التشغيل مرة أخرى عند إعادة بناء الواجهة
|
||||
_deepLinkController.deepLinkLatLng.value = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
super.onInit();
|
||||
|
||||
// // --- إضافة جديدة: تهيئة وحدة التحكم في الروابط العميقة ---
|
||||
// Get.put(DeepLinkController(), permanent: true);
|
||||
// // ----------------------------------------------------
|
||||
// مرحلة 0: الضروري جداً لعرض الخريطة سريعاً
|
||||
mapAPIKEY = await storage.read(key: BoxName.mapAPIKEY);
|
||||
await initilizeGetStorage(); // إعداد سريع
|
||||
await _initMinimalIcons(); // start/end فقط
|
||||
await addToken(); // لو لازم للمصادقة
|
||||
await _initUniLinks();
|
||||
_listenForDeepLink();
|
||||
await getLocation(); // لتحديد الكاميرا
|
||||
box.write(BoxName.carType, 'yet');
|
||||
box.write(BoxName.tipPercentage, '0');
|
||||
|
||||
@@ -179,6 +179,8 @@ class MyTranslation extends Translations {
|
||||
"Contacts Loaded": "تم تحميل جهات الاتصال",
|
||||
"Showing": "يتم عرض",
|
||||
"of": "من",
|
||||
'Pay by MTN Wallet': 'الدفع عبر محفظة MTN',
|
||||
'Pay by Syriatel Wallet': 'الدفع عبر محفظة سيريتل',
|
||||
"Customer not found": "العميل غير موجود",
|
||||
"Wallet is blocked": "المحفظة محظورة",
|
||||
"Customer phone is not active": "هاتف العميل غير نشط",
|
||||
|
||||
@@ -819,6 +819,162 @@ class PaymentController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> payWithSyriaTelWallet(
|
||||
BuildContext context, String amount, String currency) async {
|
||||
// Show a loading indicator for better user experience
|
||||
Get.dialog(const Center(child: CircularProgressIndicator()),
|
||||
barrierDismissible: false);
|
||||
|
||||
try {
|
||||
String phone = box.read(BoxName.phoneWallet);
|
||||
String driverID = box.read(BoxName.driverID).toString();
|
||||
String formattedAmount = double.parse(amount).toStringAsFixed(0);
|
||||
|
||||
// --- CHANGE 1: Updated log messages for clarity ---
|
||||
print("🚀 Starting Syriatel payment process");
|
||||
print(
|
||||
"📦 Payload: driverID: $driverID, amount: $formattedAmount, phone: $phone");
|
||||
|
||||
// Optional: Biometric authentication
|
||||
bool isAuthSupported = await LocalAuthentication().isDeviceSupported();
|
||||
if (isAuthSupported) {
|
||||
bool didAuthenticate = await LocalAuthentication().authenticate(
|
||||
localizedReason: 'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع',
|
||||
);
|
||||
if (!didAuthenticate) {
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
print("❌ User did not authenticate with biometrics");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// --- CHANGE 2: Updated API link and payload for starting payment ---
|
||||
// Make sure you have defined `payWithSyriatelStart` in your AppLink class
|
||||
var responseData = await CRUD().postWalletMtn(
|
||||
link: AppLink.payWithSyriatelStart, // Use the new Syriatel start link
|
||||
payload: {
|
||||
"amount": formattedAmount,
|
||||
"driverId": driverID, // Key changed from 'passengerId' to 'driverId'
|
||||
"phone": phone,
|
||||
"lang": box.read(BoxName.lang) ?? 'ar',
|
||||
},
|
||||
);
|
||||
|
||||
print("✅ Server response (start_payment.php):");
|
||||
print(responseData);
|
||||
|
||||
// Robustly parse the server's JSON response
|
||||
Map<String, dynamic> startRes;
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
startRes = responseData;
|
||||
} else if (responseData is String) {
|
||||
try {
|
||||
startRes = json.decode(responseData);
|
||||
} catch (e) {
|
||||
throw Exception(
|
||||
"Failed to parse server response. Response: $responseData");
|
||||
}
|
||||
} else {
|
||||
throw Exception("Received an unexpected data type from the server.");
|
||||
}
|
||||
|
||||
if (startRes['status'] != 'success') {
|
||||
String errorMsg = startRes['message']?.toString() ??
|
||||
"Failed to start the payment process. Please try again.";
|
||||
throw Exception(errorMsg);
|
||||
}
|
||||
|
||||
// --- CHANGE 3: Extract `transactionID` from the response ---
|
||||
// The response structure is now simpler. We only need the transaction ID.
|
||||
final messageData = startRes["message"];
|
||||
final transactionID = messageData["transactionID"].toString();
|
||||
|
||||
print("📄 TransactionID: $transactionID");
|
||||
|
||||
if (Get.isDialogOpen == true) Get.back(); // Close loading indicator
|
||||
|
||||
// Show the OTP input dialog
|
||||
String? otp = await showDialog<String>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
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(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (otp == null || otp.isEmpty) {
|
||||
print("❌ OTP was not entered.");
|
||||
return;
|
||||
}
|
||||
print("🔐 OTP entered: $otp");
|
||||
|
||||
Get.dialog(const Center(child: CircularProgressIndicator()),
|
||||
barrierDismissible: false);
|
||||
|
||||
// --- CHANGE 4: Updated API link and payload for confirming payment ---
|
||||
// Make sure you have defined `payWithSyriatelConfirm` in your AppLink class
|
||||
var confirmRes = await CRUD().postWallet(
|
||||
// Changed from postWalletMtn if they are different
|
||||
link:
|
||||
AppLink.payWithSyriatelConfirm, // Use the new Syriatel confirm link
|
||||
payload: {
|
||||
"transactionID": transactionID, // Use the transaction ID
|
||||
"otp": otp,
|
||||
// The other parameters (phone, guid, etc.) are no longer needed
|
||||
},
|
||||
);
|
||||
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
|
||||
print("✅ Response from confirm_payment.php:");
|
||||
Log.print('confirmRes: ${confirmRes}');
|
||||
|
||||
if (confirmRes != null && confirmRes['status'] == 'success') {
|
||||
Get.defaultDialog(
|
||||
title: "✅ نجاح",
|
||||
content: const Text("تمت عملية الدفع وإضافة الرصيد إلى محفظتك."),
|
||||
);
|
||||
} else {
|
||||
// --- CHANGE 5: Simplified error message extraction ---
|
||||
// The new PHP script sends the error directly in the 'message' field.
|
||||
String errorMsg =
|
||||
confirmRes?['message']?.toString() ?? "فشل في تأكيد الدفع";
|
||||
Get.defaultDialog(
|
||||
title: "❌ فشل",
|
||||
content: Text(errorMsg.tr),
|
||||
);
|
||||
}
|
||||
} catch (e, s) {
|
||||
// --- CHANGE 6: Updated general error log message ---
|
||||
print("🔥 Error during Syriatel Wallet payment:");
|
||||
print(e);
|
||||
print(s);
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
Get.defaultDialog(
|
||||
title: 'حدث خطأ',
|
||||
content: Text(e.toString().replaceFirst("Exception: ", "")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
timestamp = now.millisecondsSinceEpoch;
|
||||
|
||||
@@ -25,6 +25,7 @@ import 'controller/firebase/firbase_messge.dart';
|
||||
import 'controller/firebase/local_notification.dart';
|
||||
import 'controller/functions/encrypt_decrypt.dart';
|
||||
import 'controller/functions/secure_storage.dart';
|
||||
import 'controller/home/deep_link_controller.dart';
|
||||
import 'controller/local/local_controller.dart';
|
||||
import 'controller/local/translations.dart';
|
||||
import 'controller/payment/paymob/paymob_wallet.dart';
|
||||
@@ -52,7 +53,9 @@ void main() async {
|
||||
WakelockPlus.enable();
|
||||
|
||||
await GetStorage.init();
|
||||
|
||||
// --- إضافة جديدة: تهيئة وحدة التحكم في الروابط العميقة ---
|
||||
Get.put(DeepLinkController(), permanent: true);
|
||||
// ----------------------------------------------------
|
||||
final AppInitializer initializer = AppInitializer();
|
||||
|
||||
await initializer.initializeApp();
|
||||
|
||||
@@ -339,35 +339,20 @@ void showPaymentOptions(BuildContext context, PaymentController controller) {
|
||||
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(
|
||||
child: MyTextForm(
|
||||
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),
|
||||
label: 'Insert Wallet phone number'.tr,
|
||||
hint: '963941234567',
|
||||
type: TextInputType.phone)),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'OK'.tr,
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
if (controller.formKey.currentState!.validate()) {
|
||||
if (controller.selectedAmount != 0) {
|
||||
controller.isLoading = true;
|
||||
@@ -392,17 +377,71 @@ void showPaymentOptions(BuildContext context, PaymentController controller) {
|
||||
);
|
||||
}
|
||||
}
|
||||
}));
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Pay by MTN Wallet'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/images/mtn.png',
|
||||
const SizedBox(width: 10),
|
||||
Image.asset(
|
||||
'assets/images/cashMTN.png',
|
||||
width: 70,
|
||||
height: 70,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
Get.back();
|
||||
Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: 'Insert Wallet phone number'.tr,
|
||||
content: Form(
|
||||
key: controller.formKey,
|
||||
child: MyTextForm(
|
||||
controller: controller.walletphoneController,
|
||||
label: 'Insert Wallet phone number'.tr,
|
||||
hint: '963941234567',
|
||||
type: TextInputType.phone)),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'OK'.tr,
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
if (controller.formKey.currentState!.validate()) {
|
||||
box.write(BoxName.phoneWallet,
|
||||
controller.walletphoneController.text);
|
||||
await controller.payWithSyriaTelWallet(context,
|
||||
controller.selectedAmount.toString(), 'SYP');
|
||||
}
|
||||
}));
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Pay by Syriatel Wallet'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Image.asset(
|
||||
'assets/images/syriatel.png',
|
||||
width: 70,
|
||||
height: 70,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
child: Text('Cancel'.tr),
|
||||
|
||||
64
pubspec.lock
64
pubspec.lock
@@ -33,6 +33,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.3"
|
||||
app_links:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: app_links
|
||||
sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.1"
|
||||
app_links_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: app_links_linux
|
||||
sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
app_links_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: app_links_platform_interface
|
||||
sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
app_links_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: app_links_web
|
||||
sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -992,6 +1024,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
gtk:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: gtk
|
||||
sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1940,30 +1980,6 @@ 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:
|
||||
|
||||
@@ -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
|
||||
@@ -77,6 +77,7 @@ dependencies:
|
||||
asn1lib: ^1.6.5
|
||||
internet_connection_checker: ^3.0.1
|
||||
connectivity_plus: ^6.1.5
|
||||
app_links: ^6.4.1
|
||||
# home_widget: ^0.7.0+1
|
||||
|
||||
dev_dependencies:
|
||||
|
||||
Reference in New Issue
Block a user