Initial push to my private server
This commit is contained in:
@@ -46,8 +46,8 @@ android {
|
|||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
minSdk = 29
|
minSdk = 29
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 13
|
versionCode = 14
|
||||||
versionName = '1.0.13'
|
versionName = '1.0.14'
|
||||||
multiDexEnabled =true
|
multiDexEnabled =true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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/shamCash.png
Normal file
BIN
assets/images/shamCash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
assets/images/syriatel.jpeg
Normal file
BIN
assets/images/syriatel.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
@@ -11,6 +11,7 @@ class BoxName {
|
|||||||
"rideArgumentsFromBackground";
|
"rideArgumentsFromBackground";
|
||||||
static const String FCM_PRIVATE_KEY = "FCM_PRIVATE_KEY";
|
static const String FCM_PRIVATE_KEY = "FCM_PRIVATE_KEY";
|
||||||
static const String hmac = "hmac";
|
static const String hmac = "hmac";
|
||||||
|
static const String walletType = "walletType";
|
||||||
static const String fingerPrint = "fingerPrint";
|
static const String fingerPrint = "fingerPrint";
|
||||||
static const String updateInterval = "updateInterval";
|
static const String updateInterval = "updateInterval";
|
||||||
static const String payMobApikey = "payMobApikey";
|
static const String payMobApikey = "payMobApikey";
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ class AppLink {
|
|||||||
static String getDriverToken = "$ride/firebase/getDriverToken.php";
|
static String getDriverToken = "$ride/firebase/getDriverToken.php";
|
||||||
static String addTokens = "$ride/firebase/add.php";
|
static String addTokens = "$ride/firebase/add.php";
|
||||||
static String addTokensDriver = "$ride/firebase/addDriver.php";
|
static String addTokensDriver = "$ride/firebase/addDriver.php";
|
||||||
|
static String addTokensDriverWallet =
|
||||||
|
"$seferPaymentServer/ride/firebase/addDriver.php";
|
||||||
|
|
||||||
//=======================Wallet===================
|
//=======================Wallet===================
|
||||||
static String wallet = '$seferPaymentServer/ride/passengerWallet';
|
static String wallet = '$seferPaymentServer/ride/passengerWallet';
|
||||||
@@ -57,6 +59,10 @@ class AppLink {
|
|||||||
"$seferPaymentServer/ride/mtn/driver/confirm_payment.php";
|
"$seferPaymentServer/ride/mtn/driver/confirm_payment.php";
|
||||||
static String payWithMTNStart =
|
static String payWithMTNStart =
|
||||||
"$seferPaymentServer/ride/mtn/driver/mtn_start.php";
|
"$seferPaymentServer/ride/mtn/driver/mtn_start.php";
|
||||||
|
static String payWithSyriatelConfirm =
|
||||||
|
"$seferPaymentServer/ride/syriatel/driver/confirm_payment.php";
|
||||||
|
static String payWithSyriatelStart =
|
||||||
|
"$seferPaymentServer/ride/syriatel/driver/start_payment.php";
|
||||||
static String payWithEcashDriver =
|
static String payWithEcashDriver =
|
||||||
"$seferPaymentServer/ride/ecash/driver/payWithEcash.php";
|
"$seferPaymentServer/ride/ecash/driver/payWithEcash.php";
|
||||||
static String payWithEcashPassenger =
|
static String payWithEcashPassenger =
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import 'package:location/location.dart';
|
|||||||
|
|
||||||
import '../../../constant/api_key.dart';
|
import '../../../constant/api_key.dart';
|
||||||
import '../../../constant/info.dart';
|
import '../../../constant/info.dart';
|
||||||
|
import '../../../print.dart';
|
||||||
import '../../../views/auth/captin/otp_page.dart';
|
import '../../../views/auth/captin/otp_page.dart';
|
||||||
import '../../../views/auth/captin/otp_token_page.dart';
|
import '../../../views/auth/captin/otp_token_page.dart';
|
||||||
import '../../../views/auth/syria/pending_driver_page.dart';
|
import '../../../views/auth/syria/pending_driver_page.dart';
|
||||||
@@ -98,11 +99,10 @@ class LoginDriverController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isPhoneVerified() async {
|
isPhoneVerified() async {
|
||||||
var res = await CRUD().post(link: AppLink.isPhoneVerified, payload: {
|
var res = await CRUD().post(
|
||||||
'phone_number': box.read(
|
link: AppLink.isPhoneVerified,
|
||||||
BoxName.phoneDriver,
|
payload: {'phone_number': box.read(BoxName.phoneDriver)});
|
||||||
)
|
|
||||||
});
|
|
||||||
if (res != 'failure') {
|
if (res != 'failure') {
|
||||||
// Get.offAll(() => SyrianCardAI());
|
// Get.offAll(() => SyrianCardAI());
|
||||||
Get.offAll(() => RegistrationView());
|
Get.offAll(() => RegistrationView());
|
||||||
@@ -163,8 +163,8 @@ class LoginDriverController extends GetxController {
|
|||||||
|
|
||||||
getJWT() async {
|
getJWT() async {
|
||||||
dev = Platform.isAndroid ? 'android' : 'ios';
|
dev = Platform.isAndroid ? 'android' : 'ios';
|
||||||
// Log.print(
|
Log.print(
|
||||||
// 'box.read(BoxName.firstTimeLoadKey): ${box.read(BoxName.firstTimeLoadKey)}');
|
'box.read(BoxName.firstTimeLoadKey): ${box.read(BoxName.firstTimeLoadKey)}');
|
||||||
if (box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
|
if (box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
|
||||||
var payload = {
|
var payload = {
|
||||||
'id': box.read(BoxName.driverID) ?? AK.newId,
|
'id': box.read(BoxName.driverID) ?? AK.newId,
|
||||||
@@ -185,13 +185,6 @@ class LoginDriverController extends GetxController {
|
|||||||
|
|
||||||
final jwt = decodedResponse1['jwt'];
|
final jwt = decodedResponse1['jwt'];
|
||||||
box.write(BoxName.jwt, c(jwt));
|
box.write(BoxName.jwt, c(jwt));
|
||||||
// await box.write(BoxName.hmac, decodedResponse1['hmac']);
|
|
||||||
|
|
||||||
// await AppInitializer().getAIKey(Driver.payMobApikey);
|
|
||||||
// await AppInitializer().getAIKey(Driver.FCM_PRIVATE_KEY);
|
|
||||||
|
|
||||||
// await AppInitializer().getAIKey(Driver.initializationVector);
|
|
||||||
// await AppInitializer().getAIKey(Driver.keyOfApp);
|
|
||||||
|
|
||||||
// ✅ بعد التأكد أن كل المفاتيح موجودة
|
// ✅ بعد التأكد أن كل المفاتيح موجودة
|
||||||
await EncryptionHelper.initialize();
|
await EncryptionHelper.initialize();
|
||||||
@@ -206,7 +199,7 @@ class LoginDriverController extends GetxController {
|
|||||||
'password': box.read(BoxName.emailDriver),
|
'password': box.read(BoxName.emailDriver),
|
||||||
'aud': '${AK.allowed}$dev',
|
'aud': '${AK.allowed}$dev',
|
||||||
};
|
};
|
||||||
// print(payload);
|
print(payload);
|
||||||
var response1 = await http.post(
|
var response1 = await http.post(
|
||||||
Uri.parse(AppLink.loginJwtDriver),
|
Uri.parse(AppLink.loginJwtDriver),
|
||||||
body: payload,
|
body: payload,
|
||||||
@@ -332,32 +325,30 @@ class LoginDriverController extends GetxController {
|
|||||||
key: BoxName.fingerPrint, value: fingerPrint.toString());
|
key: BoxName.fingerPrint, value: fingerPrint.toString());
|
||||||
// print(jsonDecode(token)['data'][0]['token'].toString());
|
// print(jsonDecode(token)['data'][0]['token'].toString());
|
||||||
// print(box.read(BoxName.tokenDriver).toString());
|
// print(box.read(BoxName.tokenDriver).toString());
|
||||||
if (email == '962798583052@intaleqapp.com') {
|
|
||||||
} else {
|
if (token != 'failure') {
|
||||||
if (token != 'failure') {
|
if ((jsonDecode(token)['data'][0]['token'].toString()) !=
|
||||||
if ((jsonDecode(token)['data'][0]['token'].toString()) !=
|
box.read(BoxName.tokenDriver).toString()) {
|
||||||
box.read(BoxName.tokenDriver).toString()) {
|
await Get.defaultDialog(
|
||||||
await Get.defaultDialog(
|
barrierDismissible: false,
|
||||||
barrierDismissible: false,
|
title: 'Device Change Detected'.tr,
|
||||||
title: 'Device Change Detected'.tr,
|
middleText: 'Please verify your identity'.tr,
|
||||||
middleText: 'Please verify your identity'.tr,
|
textConfirm: 'Verify'.tr,
|
||||||
textConfirm: 'Verify'.tr,
|
confirmTextColor: Colors.white,
|
||||||
confirmTextColor: Colors.white,
|
onConfirm: () {
|
||||||
onConfirm: () {
|
// Get.back();
|
||||||
// Get.back();
|
// انتقل لصفحة OTP الجديدة
|
||||||
// انتقل لصفحة OTP الجديدة
|
Get.to(
|
||||||
Get.to(
|
() => OtpVerificationPage(
|
||||||
() => OtpVerificationPage(
|
phone: d['phone'].toString(),
|
||||||
phone: d['phone'].toString(),
|
deviceToken: fingerPrint.toString(),
|
||||||
deviceToken: fingerPrint.toString(),
|
token: token.toString(),
|
||||||
token: token.toString(),
|
ptoken:
|
||||||
ptoken:
|
jsonDecode(token)['data'][0]['token'].toString(),
|
||||||
jsonDecode(token)['data'][0]['token'].toString(),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,15 +86,28 @@ class OtpVerificationController extends GetxController {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response != 'failure' && response['status'] == 'success') {
|
if (response != 'failure') {
|
||||||
Get.back(); // توجه إلى الصفحة التالية
|
Get.back(); // توجه إلى الصفحة التالية
|
||||||
Get.put(FirebaseMessagesController()).sendNotificationToDriverMAP(
|
await CRUD().post(
|
||||||
|
link:
|
||||||
|
'${AppLink.seferPaymentServer}/auth/token/update_driver_auth.php',
|
||||||
|
payload: {
|
||||||
|
'token': box.read(BoxName.tokenDriver).toString(),
|
||||||
|
'fingerPrint': finger.toString(),
|
||||||
|
'captain_id': box.read(BoxName.driverID).toString(),
|
||||||
|
});
|
||||||
|
final fcm = Get.isRegistered<FirebaseMessagesController>()
|
||||||
|
? Get.find<FirebaseMessagesController>()
|
||||||
|
: Get.put(FirebaseMessagesController());
|
||||||
|
|
||||||
|
await fcm.sendNotificationToDriverMAP(
|
||||||
'token change',
|
'token change',
|
||||||
'change device'.tr,
|
'change device'.tr,
|
||||||
ptoken.toString(),
|
ptoken.toString(),
|
||||||
[],
|
[],
|
||||||
'cancel.wav',
|
'cancel.wav',
|
||||||
);
|
);
|
||||||
|
|
||||||
Get.offAll(() => HomeCaptain());
|
Get.offAll(() => HomeCaptain());
|
||||||
} else {
|
} else {
|
||||||
Get.snackbar('Verification Failed', 'OTP is incorrect or expired');
|
Get.snackbar('Verification Failed', 'OTP is incorrect or expired');
|
||||||
|
|||||||
@@ -94,22 +94,8 @@ class GoogleSignInHelper {
|
|||||||
|
|
||||||
static Future<void> _handleSignOut() async {
|
static Future<void> _handleSignOut() async {
|
||||||
// Clear stored driver information
|
// Clear stored driver information
|
||||||
|
box.erase();
|
||||||
box.remove(BoxName.driverID);
|
storage.deleteAll();
|
||||||
box.remove(BoxName.emailDriver);
|
|
||||||
box.remove(BoxName.lang);
|
|
||||||
box.remove(BoxName.nameDriver);
|
|
||||||
box.remove(BoxName.passengerID);
|
|
||||||
box.remove(BoxName.phoneDriver);
|
|
||||||
box.remove(BoxName.tokenFCM);
|
|
||||||
box.remove(BoxName.tokens);
|
|
||||||
box.remove(BoxName.carPlate);
|
|
||||||
box.remove(BoxName.lastNameDriver);
|
|
||||||
box.remove(BoxName.agreeTerms);
|
|
||||||
box.remove(BoxName.tokenDriver);
|
|
||||||
box.remove(BoxName.countryCode);
|
|
||||||
box.remove(BoxName.accountIdStripeConnect);
|
|
||||||
box.remove(BoxName.phoneVerified);
|
|
||||||
Get.offAll(OnBoardingPage());
|
Get.offAll(OnBoardingPage());
|
||||||
// Perform any additional sign-out tasks or API calls here
|
// Perform any additional sign-out tasks or API calls here
|
||||||
// For example, you can notify your server about the user sign-out
|
// For example, you can notify your server about the user sign-out
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:image_cropper/image_cropper.dart';
|
import 'package:image_cropper/image_cropper.dart';
|
||||||
import 'package:image/image.dart' as img;
|
import 'package:image/image.dart' as img;
|
||||||
|
import 'package:path/path.dart';
|
||||||
import 'package:sefer_driver/constant/links.dart';
|
import 'package:sefer_driver/constant/links.dart';
|
||||||
import '../../../constant/box_name.dart';
|
import '../../../constant/box_name.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
// --- Final Submission ---
|
// --- Final Submission ---
|
||||||
import 'dart:convert';
|
import 'package:path_provider/path_provider.dart' as path_provider;
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:http_parser/http_parser.dart';
|
import 'package:http_parser/http_parser.dart';
|
||||||
import 'package:mime/mime.dart';
|
import 'package:mime/mime.dart';
|
||||||
@@ -21,8 +21,10 @@ import '../../../constant/colors.dart';
|
|||||||
import '../../../constant/info.dart';
|
import '../../../constant/info.dart';
|
||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
import '../../../print.dart';
|
import '../../../print.dart';
|
||||||
|
import '../../../views/widgets/error_snakbar.dart';
|
||||||
import '../../functions/crud.dart';
|
import '../../functions/crud.dart';
|
||||||
import '../../functions/encrypt_decrypt.dart';
|
import '../../functions/encrypt_decrypt.dart';
|
||||||
|
import '../../functions/package_info.dart';
|
||||||
import '../captin/login_captin_controller.dart';
|
import '../captin/login_captin_controller.dart';
|
||||||
|
|
||||||
// You can create a simple enum to manage image types
|
// You can create a simple enum to manage image types
|
||||||
@@ -41,6 +43,11 @@ class RegistrationController extends GetxController {
|
|||||||
|
|
||||||
// Loading state
|
// Loading state
|
||||||
var isLoading = false.obs;
|
var isLoading = false.obs;
|
||||||
|
var isloading = false;
|
||||||
|
CroppedFile? croppedFile;
|
||||||
|
final picker = ImagePicker();
|
||||||
|
var image;
|
||||||
|
File? myImage;
|
||||||
String? colorHex; // سيُملى من الدروب داون
|
String? colorHex; // سيُملى من الدروب داون
|
||||||
// Form Keys for validation
|
// Form Keys for validation
|
||||||
final driverInfoFormKey = GlobalKey<FormState>();
|
final driverInfoFormKey = GlobalKey<FormState>();
|
||||||
@@ -233,6 +240,142 @@ class RegistrationController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// خريطة لتخزين روابط المستندات بعد الرفع
|
||||||
|
final Map<String, String> docUrls = {
|
||||||
|
'driver_license_front': '',
|
||||||
|
'driver_license_back': '',
|
||||||
|
'car_license_front': '',
|
||||||
|
'car_license_back': '',
|
||||||
|
};
|
||||||
|
|
||||||
|
/// التصرّف العام لاختيار/قص/ضغط/رفع الصورة حسب type
|
||||||
|
Future<void> choosImage(String link, String imageType) async {
|
||||||
|
try {
|
||||||
|
final pickedImage = await picker.pickImage(
|
||||||
|
source: ImageSource.camera,
|
||||||
|
preferredCameraDevice: CameraDevice.rear,
|
||||||
|
);
|
||||||
|
if (pickedImage == null) return;
|
||||||
|
|
||||||
|
image = File(pickedImage.path);
|
||||||
|
|
||||||
|
final croppedFile = await ImageCropper().cropImage(
|
||||||
|
sourcePath: image!.path,
|
||||||
|
uiSettings: [
|
||||||
|
AndroidUiSettings(
|
||||||
|
toolbarTitle: 'Cropper'.tr,
|
||||||
|
toolbarColor: AppColor.blueColor,
|
||||||
|
toolbarWidgetColor: AppColor.yellowColor,
|
||||||
|
initAspectRatio: CropAspectRatioPreset.original,
|
||||||
|
lockAspectRatio: false,
|
||||||
|
),
|
||||||
|
IOSUiSettings(title: 'Cropper'.tr),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
if (croppedFile == null) return;
|
||||||
|
|
||||||
|
// صورة للمعاينة داخل التطبيق
|
||||||
|
myImage = File(croppedFile.path);
|
||||||
|
|
||||||
|
isloading = true;
|
||||||
|
update();
|
||||||
|
|
||||||
|
// ضغط (وأيضاً يمكنك إضافة rotateImageIfNeeded قبل/بعد الضغط إن رغبت)
|
||||||
|
final File compressedImage = await compressImage(File(croppedFile.path));
|
||||||
|
|
||||||
|
// تجهيز الحقول
|
||||||
|
final driverId = box.read(BoxName.driverID);
|
||||||
|
|
||||||
|
final payload = <String, String>{
|
||||||
|
'driverID': driverId,
|
||||||
|
'imageType': imageType, // مثال: driver_license_front
|
||||||
|
};
|
||||||
|
|
||||||
|
// الرفع وإرجاع الرابط
|
||||||
|
final String imageUrl = await uploadImage(compressedImage, payload, link);
|
||||||
|
|
||||||
|
// حفظ الرابط محلياً حسب النوع
|
||||||
|
docUrls[imageType] = imageUrl;
|
||||||
|
|
||||||
|
Log.print('✅ Uploaded $imageType => $imageUrl');
|
||||||
|
} catch (e, st) {
|
||||||
|
Log.print('❌ Error in choosImage: $e\n$st');
|
||||||
|
mySnackeBarError('Image Upload Failed'.tr);
|
||||||
|
} finally {
|
||||||
|
isloading = false;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ترفع الملف وترجع رابط الصورة النهائي كـ String
|
||||||
|
Future<String> uploadImage(
|
||||||
|
File file, Map<String, String> data, String link) async {
|
||||||
|
final uri = Uri.parse(link);
|
||||||
|
final request = http.MultipartRequest('POST', uri);
|
||||||
|
|
||||||
|
// الهيدرز (كما عندك)
|
||||||
|
final headers = <String, String>{
|
||||||
|
'Authorization':
|
||||||
|
'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}',
|
||||||
|
'X-HMAC-Auth': '${box.read(BoxName.hmac)}',
|
||||||
|
};
|
||||||
|
request.headers.addAll(headers);
|
||||||
|
|
||||||
|
// اسم الملف: driverID.jpg (اختياري)
|
||||||
|
final forcedName = '${box.read(BoxName.driverID) ?? 'image'}.jpg';
|
||||||
|
|
||||||
|
// إضافة الملف (من المسار مباشرة أسلم من الـ stream)
|
||||||
|
request.files.add(
|
||||||
|
await http.MultipartFile.fromPath(
|
||||||
|
'image', // تأكد أنه نفس اسم الحقل على السيرفر
|
||||||
|
file.path,
|
||||||
|
filename: forcedName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// الحقول الإضافية
|
||||||
|
data.forEach((k, v) => request.fields[k] = v);
|
||||||
|
|
||||||
|
// الإرسال
|
||||||
|
final streamed = await request.send();
|
||||||
|
final res = await http.Response.fromStream(streamed);
|
||||||
|
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
throw Exception(
|
||||||
|
'Failed to upload image: ${res.statusCode} - ${res.body}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// نحاول استخراج رابط الصورة من أكثر من مفتاح محتمل
|
||||||
|
final body = jsonDecode(res.body);
|
||||||
|
final String? url = body['url'] ??
|
||||||
|
body['file_link'] ??
|
||||||
|
body['image_url'] ??
|
||||||
|
(body['data'] is Map ? body['data']['url'] : null);
|
||||||
|
|
||||||
|
if (url == null || url.isEmpty) {
|
||||||
|
// لو السيرفر يرجع هيكل مختلف، عدّل هنا المفتاح حسب استجابتك الفعلية
|
||||||
|
throw Exception(
|
||||||
|
'Upload succeeded but no image URL found in response: ${res.body}');
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<File> compressImage(File file) async {
|
||||||
|
final dir = await path_provider.getTemporaryDirectory();
|
||||||
|
final targetPath = "${dir.absolute.path}/temp.jpg";
|
||||||
|
|
||||||
|
var result = await FlutterImageCompress.compressAndGetFile(
|
||||||
|
file.absolute.path,
|
||||||
|
targetPath,
|
||||||
|
quality: 70,
|
||||||
|
minWidth: 1024,
|
||||||
|
minHeight: 1024,
|
||||||
|
);
|
||||||
|
|
||||||
|
return File(result!.path);
|
||||||
|
}
|
||||||
|
|
||||||
// دالة رفع إلى السيرفر السوري: ترجع file_url (Signed URL)
|
// دالة رفع إلى السيرفر السوري: ترجع file_url (Signed URL)
|
||||||
Future<String> uploadToSyria({
|
Future<String> uploadToSyria({
|
||||||
required String docType,
|
required String docType,
|
||||||
@@ -317,30 +460,17 @@ class RegistrationController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> submitRegistration() async {
|
Future<void> submitRegistration() async {
|
||||||
// 1) تحقق من الصور
|
// 0) دوال/مساعدات محلية
|
||||||
if (driverLicenseFrontImage == null ||
|
|
||||||
driverLicenseBackImage == null ||
|
// 1) تحقق من وجود الروابط بدل الملفات
|
||||||
carLicenseFrontImage == null ||
|
final driverFrontUrl = docUrls['driver_license_front'];
|
||||||
carLicenseBackImage == null) {
|
final driverBackUrl = docUrls['driver_license_back'];
|
||||||
Get.snackbar(
|
final carFrontUrl = docUrls['car_license_front'];
|
||||||
'Missing Documents'.tr,
|
final carBackUrl = docUrls['car_license_back'];
|
||||||
'Please upload all 4 required documents.'.tr,
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
backgroundColor: Colors.orange,
|
|
||||||
colorText: Colors.white,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
// روابط الـ API
|
final registerUri = Uri.parse(AppLink.register_driver_and_car);
|
||||||
final registerUri =
|
|
||||||
Uri.parse(AppLink.register_driver_and_car); // التسجيل الرئيسي (PHP)
|
|
||||||
final syrianUploadUri =
|
|
||||||
// Uri.parse(AppLink.uploadSyrianDocs); // رفع الصور في سوريا
|
|
||||||
Uri.parse(
|
|
||||||
'https://syria.intaleq.xyz/intaleq/auth/syria/uploadSyrianDocs.php'); // رفع الصور في سوريا
|
|
||||||
|
|
||||||
final client = http.Client();
|
final client = http.Client();
|
||||||
try {
|
try {
|
||||||
@@ -349,50 +479,7 @@ class RegistrationController extends GetxController {
|
|||||||
'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}';
|
'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}';
|
||||||
final hmac = '${box.read(BoxName.hmac)}';
|
final hmac = '${box.read(BoxName.hmac)}';
|
||||||
|
|
||||||
// 2) ارفع الصور أولاً على السيرفر السوري واحصل على روابطها (Signed URLs)
|
// 2) جهّز طلب التسجيل الرئيسي: حقول فقط + روابط الصور (لا نرفع صور إطلاقًا)
|
||||||
final driverId = (box.read(BoxName.driverID) ?? '').toString();
|
|
||||||
|
|
||||||
final driverFrontUrl = await uploadToSyria(
|
|
||||||
docType: 'driver_license_front',
|
|
||||||
file: driverLicenseFrontImage!,
|
|
||||||
syrianUploadUri: syrianUploadUri,
|
|
||||||
authHeader: bearer,
|
|
||||||
hmacHeader: hmac,
|
|
||||||
driverId: driverId,
|
|
||||||
clientOverride: client,
|
|
||||||
);
|
|
||||||
|
|
||||||
final driverBackUrl = await uploadToSyria(
|
|
||||||
docType: 'driver_license_back',
|
|
||||||
file: driverLicenseBackImage!,
|
|
||||||
syrianUploadUri: syrianUploadUri,
|
|
||||||
authHeader: bearer,
|
|
||||||
hmacHeader: hmac,
|
|
||||||
driverId: driverId,
|
|
||||||
clientOverride: client,
|
|
||||||
);
|
|
||||||
|
|
||||||
final carFrontUrl = await uploadToSyria(
|
|
||||||
docType: 'car_license_front',
|
|
||||||
file: carLicenseFrontImage!,
|
|
||||||
syrianUploadUri: syrianUploadUri,
|
|
||||||
authHeader: bearer,
|
|
||||||
hmacHeader: hmac,
|
|
||||||
driverId: driverId,
|
|
||||||
clientOverride: client,
|
|
||||||
);
|
|
||||||
|
|
||||||
final carBackUrl = await uploadToSyria(
|
|
||||||
docType: 'car_license_back',
|
|
||||||
file: carLicenseBackImage!,
|
|
||||||
syrianUploadUri: syrianUploadUri,
|
|
||||||
authHeader: bearer,
|
|
||||||
hmacHeader: hmac,
|
|
||||||
driverId: driverId,
|
|
||||||
clientOverride: client,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3) جهّز طلب التسجيل الرئيسي: نرسل الحقول + روابط الصور (لا نرفع الصور مرة ثانية)
|
|
||||||
final req = http.MultipartRequest('POST', registerUri);
|
final req = http.MultipartRequest('POST', registerUri);
|
||||||
req.headers.addAll({
|
req.headers.addAll({
|
||||||
'Authorization': bearer,
|
'Authorization': bearer,
|
||||||
@@ -411,18 +498,16 @@ class RegistrationController extends GetxController {
|
|||||||
_addField(
|
_addField(
|
||||||
fields, 'password', 'generate_your_password_here'); // عدّل حسب منطقك
|
fields, 'password', 'generate_your_password_here'); // عدّل حسب منطقك
|
||||||
_addField(fields, 'status', 'yet');
|
_addField(fields, 'status', 'yet');
|
||||||
_addField(fields, 'email',
|
_addField(fields, 'email', 'Not specified');
|
||||||
'Not specified'); // السيرفر سيحوّلها null ويبني ايميل افتراضي
|
|
||||||
_addField(fields, 'gender', 'Male');
|
_addField(fields, 'gender', 'Male');
|
||||||
|
|
||||||
// --- Car Data ---
|
// --- Car Data ---
|
||||||
_addField(fields, 'vin', 'yet'); // تم تصحيح الاقتباس
|
_addField(fields, 'vin', 'yet');
|
||||||
_addField(fields, 'car_plate', carPlateController.text);
|
_addField(fields, 'car_plate', carPlateController.text);
|
||||||
_addField(fields, 'make', carMakeController.text);
|
_addField(fields, 'make', carMakeController.text);
|
||||||
_addField(fields, 'model', carModelController.text);
|
_addField(fields, 'model', carModelController.text);
|
||||||
_addField(fields, 'year', carYearController.text);
|
_addField(fields, 'year', carYearController.text);
|
||||||
_addField(fields, 'expiration_date',
|
_addField(fields, 'expiration_date', driverLicenseExpiryController.text);
|
||||||
driverLicenseExpiryController.text); // تم التصحيح
|
|
||||||
_addField(fields, 'color', carColorController.text);
|
_addField(fields, 'color', carColorController.text);
|
||||||
_addField(fields, 'fuel', 'Gasoline');
|
_addField(fields, 'fuel', 'Gasoline');
|
||||||
if (colorHex != null && colorHex!.isNotEmpty) {
|
if (colorHex != null && colorHex!.isNotEmpty) {
|
||||||
@@ -431,32 +516,32 @@ class RegistrationController extends GetxController {
|
|||||||
_addField(fields, 'owner',
|
_addField(fields, 'owner',
|
||||||
'${firstNameController.text} ${lastNameController.text}');
|
'${firstNameController.text} ${lastNameController.text}');
|
||||||
|
|
||||||
// --- روابط الصور الموقّعة من سوريا ---
|
// --- روابط الصور المخزنة مسبقًا ---
|
||||||
_addField(fields, 'driver_license_front', driverFrontUrl);
|
_addField(fields, 'driver_license_front', driverFrontUrl!);
|
||||||
_addField(fields, 'driver_license_back', driverBackUrl);
|
_addField(fields, 'driver_license_back', driverBackUrl!);
|
||||||
_addField(fields, 'car_license_front', carFrontUrl);
|
_addField(fields, 'car_license_front', carFrontUrl!);
|
||||||
_addField(fields, 'car_license_back', carBackUrl);
|
_addField(fields, 'car_license_back', carBackUrl!);
|
||||||
|
|
||||||
// أضف الحقول
|
// أضف الحقول
|
||||||
req.fields.addAll(fields);
|
req.fields.addAll(fields);
|
||||||
|
|
||||||
// 4) الإرسال
|
// 3) الإرسال
|
||||||
final streamed =
|
final streamed =
|
||||||
await client.send(req).timeout(const Duration(seconds: 60));
|
await client.send(req).timeout(const Duration(seconds: 60));
|
||||||
final resp = await http.Response.fromStream(streamed);
|
final resp = await http.Response.fromStream(streamed);
|
||||||
|
|
||||||
// 5) فحص النتيجة
|
// 4) فحص النتيجة
|
||||||
Map<String, dynamic>? json;
|
Map<String, dynamic>? json;
|
||||||
try {
|
try {
|
||||||
json = jsonDecode(resp.body) as Map<String, dynamic>;
|
json = jsonDecode(resp.body) as Map<String, dynamic>;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
if (resp.statusCode == 200 && json?['status'] == 'success') {
|
if (resp.statusCode == 200 && json?['status'] == 'success') {
|
||||||
final driverID =
|
// final driverID =
|
||||||
(json!['data']?['driverID'] ?? json['driverID'])?.toString();
|
// (json!['data']?['driverID'] ?? json['driverID'])?.toString();
|
||||||
if (driverID != null && driverID.isNotEmpty) {
|
// if (driverID != null && driverID.isNotEmpty) {
|
||||||
box.write(BoxName.driverID, driverID);
|
// box.write(BoxName.driverID, driverID);
|
||||||
}
|
// }
|
||||||
|
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Success'.tr,
|
'Success'.tr,
|
||||||
@@ -466,20 +551,31 @@ class RegistrationController extends GetxController {
|
|||||||
colorText: Colors.white,
|
colorText: Colors.white,
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: التنقّل أو تحديث الحالة…
|
// متابعة تسجيل الدخول إن لزم
|
||||||
final email = box.read<String?>(BoxName.emailDriver) ?? '';
|
final email = box.read(BoxName.emailDriver);
|
||||||
|
final driverID = box.read(BoxName.driverID);
|
||||||
final c = Get.isRegistered<LoginDriverController>()
|
final c = Get.isRegistered<LoginDriverController>()
|
||||||
? Get.find<LoginDriverController>()
|
? Get.find<LoginDriverController>()
|
||||||
: Get.put(LoginDriverController());
|
: Get.put(LoginDriverController());
|
||||||
|
//token to server
|
||||||
|
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
||||||
|
await CRUD().post(link: AppLink.addTokensDriver, payload: {
|
||||||
|
'captain_id': (box.read(BoxName.driverID)).toString(),
|
||||||
|
'token': (box.read(BoxName.tokenDriver)).toString(),
|
||||||
|
'fingerPrint': fingerPrint.toString(),
|
||||||
|
});
|
||||||
|
await CRUD().post(link: AppLink.addTokensDriverWallet, payload: {
|
||||||
|
'token': box.read(BoxName.tokenDriver).toString(),
|
||||||
|
'fingerPrint': fingerPrint.toString(),
|
||||||
|
'captain_id': box.read(BoxName.driverID).toString(),
|
||||||
|
});
|
||||||
|
|
||||||
c.loginWithGoogleCredential(driverId, email);
|
c.loginWithGoogleCredential(driverID, email);
|
||||||
} else {
|
} else {
|
||||||
final msg =
|
final msg =
|
||||||
(json?['message'] ?? 'Registration failed. Please try again.')
|
(json?['message'] ?? 'Registration failed. Please try again.')
|
||||||
.toString();
|
.toString();
|
||||||
Log.print('msg: ${msg}');
|
Log.print('msg: $msg');
|
||||||
|
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Error'.tr,
|
'Error'.tr,
|
||||||
msg,
|
msg,
|
||||||
@@ -500,8 +596,7 @@ class RegistrationController extends GetxController {
|
|||||||
client.close();
|
client.close();
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
} // Future<void> submitRegistration() async {
|
||||||
// Future<void> submitRegistration() async {
|
|
||||||
// // 1) تحقق من الصور
|
// // 1) تحقق من الصور
|
||||||
// if (driverLicenseFrontImage == null ||
|
// if (driverLicenseFrontImage == null ||
|
||||||
// driverLicenseBackImage == null ||
|
// driverLicenseBackImage == null ||
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
import 'package:jwt_decoder/jwt_decoder.dart';
|
import 'package:jwt_decoder/jwt_decoder.dart';
|
||||||
import 'package:sefer_driver/controller/functions/network/net_guard.dart';
|
import 'package:sefer_driver/controller/functions/network/net_guard.dart';
|
||||||
import 'package:secure_string_operations/secure_string_operations.dart';
|
import 'package:secure_string_operations/secure_string_operations.dart';
|
||||||
@@ -16,6 +17,7 @@ import '../../constant/info.dart';
|
|||||||
import '../../views/widgets/error_snakbar.dart';
|
import '../../views/widgets/error_snakbar.dart';
|
||||||
import '../../print.dart';
|
import '../../print.dart';
|
||||||
import 'gemeni.dart';
|
import 'gemeni.dart';
|
||||||
|
import 'network/connection_check.dart';
|
||||||
import 'upload_image.dart';
|
import 'upload_image.dart';
|
||||||
|
|
||||||
class CRUD {
|
class CRUD {
|
||||||
@@ -86,24 +88,27 @@ class CRUD {
|
|||||||
Map<String, dynamic>? payload,
|
Map<String, dynamic>? payload,
|
||||||
required Map<String, String> headers,
|
required Map<String, String> headers,
|
||||||
}) async {
|
}) async {
|
||||||
// ✅ 1. Check for internet connection before making any request.
|
|
||||||
if (!await _netGuard.hasInternet(mustReach: Uri.parse(link))) {
|
|
||||||
// ✅ 2. If no internet, show a notification to the user (only once every 15s).
|
|
||||||
_netGuard.notifyOnce((title, msg) {
|
|
||||||
mySnackeBarError(
|
|
||||||
msg); // Using your existing snackbar for notifications.
|
|
||||||
});
|
|
||||||
// ✅ 3. Return a specific status to indicate no internet.
|
|
||||||
return 'no_internet';
|
|
||||||
}
|
|
||||||
|
|
||||||
var url = Uri.parse(link);
|
|
||||||
try {
|
try {
|
||||||
var response = await http.post(
|
// 1. Wrap the http.post call directly with HttpRetry.sendWithRetry.
|
||||||
url,
|
// It will attempt the request immediately and retry on transient errors.
|
||||||
body: payload,
|
var response = await HttpRetry.sendWithRetry(
|
||||||
headers: headers,
|
() {
|
||||||
|
var url = Uri.parse(link);
|
||||||
|
return http.post(
|
||||||
|
url,
|
||||||
|
body: payload,
|
||||||
|
headers: headers,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
// Optional: you can customize retry behavior for each call
|
||||||
|
maxRetries: 3,
|
||||||
|
timeout: const Duration(seconds: 15),
|
||||||
);
|
);
|
||||||
|
// Log.print('response: ${response.body}');
|
||||||
|
// Log.print('request: ${response.request}');
|
||||||
|
// Log.print('payload: ${payload}');
|
||||||
|
// ✅ All your existing logic for handling server responses remains the same.
|
||||||
|
// This part is only reached if the network request itself was successful.
|
||||||
|
|
||||||
// Handle successful response (200 OK)
|
// Handle successful response (200 OK)
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
@@ -112,16 +117,18 @@ class CRUD {
|
|||||||
if (jsonData['status'] == 'success') {
|
if (jsonData['status'] == 'success') {
|
||||||
return jsonData; // Return the full JSON object on success
|
return jsonData; // Return the full JSON object on success
|
||||||
} else {
|
} else {
|
||||||
// The API reported a logical failure (e.g., validation error)
|
if (jsonData['status'] == 'failure') {
|
||||||
addError(
|
// return 'failure';
|
||||||
'API Logic Error: ${jsonData['status']}',
|
} else {
|
||||||
'Response: ${response.body}',
|
addError(
|
||||||
'CRUD._makeRequest - $link',
|
'API Logic Error: ${jsonData['status']}',
|
||||||
);
|
'Response: ${response.body}',
|
||||||
|
'CRUD._makeRequest - $link',
|
||||||
|
);
|
||||||
|
}
|
||||||
return jsonData['status']; // Return the specific status string
|
return jsonData['status']; // Return the specific status string
|
||||||
}
|
}
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
// Error decoding the JSON response from the server
|
|
||||||
addError(
|
addError(
|
||||||
'JSON Decode Error: $e',
|
'JSON Decode Error: $e',
|
||||||
'Response Body: ${response.body}\nStack Trace: $stackTrace',
|
'Response Body: ${response.body}\nStack Trace: $stackTrace',
|
||||||
@@ -130,20 +137,13 @@ class CRUD {
|
|||||||
return 'failure';
|
return 'failure';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Handle Unauthorized (401) - typically means token expired
|
// Handle Unauthorized (401)
|
||||||
else if (response.statusCode == 401) {
|
else if (response.statusCode == 401) {
|
||||||
var jsonData = jsonDecode(response.body);
|
var jsonData = jsonDecode(response.body);
|
||||||
if (jsonData['error'] == 'Token expired') {
|
if (jsonData['error'] == 'Token expired') {
|
||||||
// The token refresh logic is handled before the call,
|
await Get.put(LoginDriverController()).getJWT();
|
||||||
// but we log this case if it still happens.
|
|
||||||
// addError(
|
|
||||||
// 'Token Expired',
|
|
||||||
// 'A new token should have been fetched before this call.',
|
|
||||||
// 'CRUD._makeRequest - $link',
|
|
||||||
// );
|
|
||||||
return 'token_expired';
|
return 'token_expired';
|
||||||
} else {
|
} else {
|
||||||
// Other 401 errors (e.g., invalid token)
|
|
||||||
addError(
|
addError(
|
||||||
'Unauthorized Error: ${jsonData['error']}',
|
'Unauthorized Error: ${jsonData['error']}',
|
||||||
'Status Code: 401',
|
'Status Code: 401',
|
||||||
@@ -161,8 +161,14 @@ class CRUD {
|
|||||||
);
|
);
|
||||||
return 'failure';
|
return 'failure';
|
||||||
}
|
}
|
||||||
|
} on SocketException {
|
||||||
|
// 2. This block now catches the "no internet" case after all retries have failed.
|
||||||
|
_netGuard.notifyOnce((title, msg) {
|
||||||
|
mySnackeBarError(msg);
|
||||||
|
});
|
||||||
|
return 'no_internet'; // Return the specific status you were using before.
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
// Handle network exceptions (e.g., no internet, DNS error)
|
// 3. This is a general catch-all for any other unexpected errors.
|
||||||
addError(
|
addError(
|
||||||
'HTTP Request Exception: $e',
|
'HTTP Request Exception: $e',
|
||||||
'Stack Trace: $stackTrace',
|
'Stack Trace: $stackTrace',
|
||||||
@@ -177,15 +183,15 @@ class CRUD {
|
|||||||
Map<String, dynamic>? payload,
|
Map<String, dynamic>? payload,
|
||||||
}) async {
|
}) async {
|
||||||
// 1. Check if the token is expired
|
// 1. Check if the token is expired
|
||||||
bool isTokenExpired = JwtDecoder.isExpired(X
|
// bool isTokenExpired = JwtDecoder.isExpired(X
|
||||||
.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs)
|
// .r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs)
|
||||||
.toString()
|
// .toString()
|
||||||
.split(AppInformation.addd)[0]);
|
// .split(AppInformation.addd)[0]);
|
||||||
|
|
||||||
// 2. If expired, get a new one
|
// // 2. If expired, get a new one
|
||||||
if (isTokenExpired) {
|
// if (isTokenExpired) {
|
||||||
await LoginDriverController().getJWT();
|
// await LoginDriverController().getJWT();
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 3. Prepare the headers with the valid token
|
// 3. Prepare the headers with the valid token
|
||||||
final headers = {
|
final headers = {
|
||||||
@@ -303,15 +309,15 @@ class CRUD {
|
|||||||
required String link,
|
required String link,
|
||||||
Map<String, dynamic>? payload,
|
Map<String, dynamic>? payload,
|
||||||
}) async {
|
}) async {
|
||||||
bool isTokenExpired = JwtDecoder.isExpired(X
|
// bool isTokenExpired = JwtDecoder.isExpired(X
|
||||||
.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs)
|
// .r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs)
|
||||||
.toString()
|
// .toString()
|
||||||
.split(AppInformation.addd)[0]);
|
// .split(AppInformation.addd)[0]);
|
||||||
// Log.print('isTokenExpired: ${isTokenExpired}');
|
// // Log.print('isTokenExpired: ${isTokenExpired}');
|
||||||
|
|
||||||
if (isTokenExpired) {
|
// if (isTokenExpired) {
|
||||||
await LoginDriverController().getJWT();
|
// await LoginDriverController().getJWT();
|
||||||
}
|
// }
|
||||||
// await Get.put(LoginDriverController()).getJWT();
|
// await Get.put(LoginDriverController()).getJWT();
|
||||||
var url = Uri.parse(
|
var url = Uri.parse(
|
||||||
link,
|
link,
|
||||||
|
|||||||
@@ -109,8 +109,7 @@ class LocationController extends GetxController {
|
|||||||
|
|
||||||
// ✅ تحديث للسيرفر
|
// ✅ تحديث للسيرفر
|
||||||
await CRUD().post(
|
await CRUD().post(
|
||||||
link:
|
link: '${AppLink.server}/ride/location/update.php',
|
||||||
box.read(BoxName.serverChosen) + '/ride/location/update.php',
|
|
||||||
payload: payload,
|
payload: payload,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -141,7 +140,7 @@ class LocationController extends GetxController {
|
|||||||
if (_insertCounter == 12) {
|
if (_insertCounter == 12) {
|
||||||
_insertCounter = 0;
|
_insertCounter = 0;
|
||||||
await CRUD().post(
|
await CRUD().post(
|
||||||
link: box.read(BoxName.serverChosen) + '/ride/location/add.php',
|
link: '${AppLink.server}/ride/location/add.php',
|
||||||
payload: payload,
|
payload: payload,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,13 +35,13 @@ Future<void> showDriverGiftClaim(BuildContext context) async {
|
|||||||
if (box.read(BoxName.is_claimed).toString() == '0' ||
|
if (box.read(BoxName.is_claimed).toString() == '0' ||
|
||||||
box.read(BoxName.is_claimed) == null) {
|
box.read(BoxName.is_claimed) == null) {
|
||||||
MyDialog().getDialog(
|
MyDialog().getDialog(
|
||||||
'You have gift 300 L.E'.tr, 'This for new registration'.tr, () async {
|
'You have gift 30000 SYP'.tr, 'This for new registration'.tr, () async {
|
||||||
var res = await CRUD().post(link: AppLink.updateDriverClaim, payload: {
|
var res = await CRUD().post(link: AppLink.updateDriverClaim, payload: {
|
||||||
'driverId': box.read(BoxName.driverID),
|
'driverId': box.read(BoxName.driverID),
|
||||||
});
|
});
|
||||||
if (res != 'failure') {
|
if (res != 'failure') {
|
||||||
Get.find<CaptainWalletController>()
|
Get.find<CaptainWalletController>()
|
||||||
.addDriverWallet('new driver', '300', '300');
|
.addDriverWallet('new driver', '30000', '30000');
|
||||||
Confetti.launch(
|
Confetti.launch(
|
||||||
context,
|
context,
|
||||||
options:
|
options:
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class SmsEgyptController extends GetxController {
|
|||||||
var res = await http.post(
|
var res = await http.post(
|
||||||
Uri.parse(AppLink.checkCredit),
|
Uri.parse(AppLink.checkCredit),
|
||||||
body: {
|
body: {
|
||||||
"username": AppInformation.appName,
|
"username": 'Sefer',
|
||||||
"password": AK.smsPasswordEgypt,
|
"password": AK.smsPasswordEgypt,
|
||||||
"message": "This is an example SMS message.",
|
"message": "This is an example SMS message.",
|
||||||
"language": box.read(BoxName.lang) == 'en' ? "e" : 'r',
|
"language": box.read(BoxName.lang) == 'en' ? "e" : 'r',
|
||||||
|
|||||||
@@ -426,9 +426,14 @@ class HomeCaptainController extends GetxController {
|
|||||||
var res = await CRUD().get(
|
var res = await CRUD().get(
|
||||||
link: AppLink.getAllPaymentFromRide,
|
link: AppLink.getAllPaymentFromRide,
|
||||||
payload: {'driverID': box.read(BoxName.driverID).toString()});
|
payload: {'driverID': box.read(BoxName.driverID).toString()});
|
||||||
data = jsonDecode(res);
|
if (res == 'failure') {
|
||||||
|
totalMoneyInSEFER = '0';
|
||||||
|
} else {
|
||||||
|
data = jsonDecode(res);
|
||||||
|
|
||||||
|
totalMoneyInSEFER = data['message'][0]['total_amount'];
|
||||||
|
}
|
||||||
|
|
||||||
totalMoneyInSEFER = data['message'][0]['total_amount'] ?? '0';
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -331,13 +331,7 @@ class MapDriverController extends GetxController {
|
|||||||
'driverGoToPassengerTime': DateTime.now().toString(),
|
'driverGoToPassengerTime': DateTime.now().toString(),
|
||||||
'status': 'Applied'
|
'status': 'Applied'
|
||||||
});
|
});
|
||||||
if (AppLink.endPoint != AppLink.seferCairoServer) {
|
|
||||||
CRUD().post(link: "${AppLink.endPoint}/ride/rides/update.php", payload: {
|
|
||||||
'id': (rideId),
|
|
||||||
'driverGoToPassengerTime': DateTime.now().toString(),
|
|
||||||
'status': 'Applied'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Get.find<HomeCaptainController>().changeToAppliedRide('Applied');
|
// Get.find<HomeCaptainController>().changeToAppliedRide('Applied');
|
||||||
|
|
||||||
Get.find<FirebaseMessagesController>().sendNotificationToDriverMAP(
|
Get.find<FirebaseMessagesController>().sendNotificationToDriverMAP(
|
||||||
|
|||||||
112
lib/controller/home/captin/navigation_service.dart
Normal file
112
lib/controller/home/captin/navigation_service.dart
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
|
import 'package:google_polyline_algorithm/google_polyline_algorithm.dart';
|
||||||
|
import 'package:sefer_driver/constant/api_key.dart';
|
||||||
|
import 'package:sefer_driver/constant/box_name.dart';
|
||||||
|
import 'package:sefer_driver/constant/links.dart';
|
||||||
|
import 'package:sefer_driver/controller/functions/crud.dart';
|
||||||
|
import 'package:sefer_driver/controller/functions/tts.dart';
|
||||||
|
|
||||||
|
import '../../../main.dart';
|
||||||
|
|
||||||
|
/// Handles map-related logic: fetching routes, drawing polylines, and managing markers.
|
||||||
|
class NavigationService extends GetxService {
|
||||||
|
final CRUD _crud = CRUD();
|
||||||
|
final TextToSpeechController _tts = Get.put(TextToSpeechController());
|
||||||
|
|
||||||
|
final RxSet<Marker> markers = <Marker>{}.obs;
|
||||||
|
final RxSet<Polyline> polylines = <Polyline>{}.obs;
|
||||||
|
final RxString currentInstruction = "".obs;
|
||||||
|
|
||||||
|
BitmapDescriptor carIcon = BitmapDescriptor.defaultMarker;
|
||||||
|
BitmapDescriptor passengerIcon = BitmapDescriptor.defaultMarker;
|
||||||
|
BitmapDescriptor startIcon = BitmapDescriptor.defaultMarker;
|
||||||
|
BitmapDescriptor endIcon = BitmapDescriptor.defaultMarker;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
_loadCustomIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadCustomIcons() async {
|
||||||
|
carIcon = await _createBitmapDescriptor('assets/images/car.png');
|
||||||
|
passengerIcon = await _createBitmapDescriptor('assets/images/picker.png');
|
||||||
|
startIcon = await _createBitmapDescriptor('assets/images/A.png');
|
||||||
|
endIcon = await _createBitmapDescriptor('assets/images/b.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<BitmapDescriptor> _createBitmapDescriptor(String assetName) {
|
||||||
|
return BitmapDescriptor.fromAssetImage(
|
||||||
|
ImageConfiguration(
|
||||||
|
size: const Size(30, 35), devicePixelRatio: Get.pixelRatio),
|
||||||
|
assetName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>?> getRoute({
|
||||||
|
required LatLng origin,
|
||||||
|
required LatLng destination,
|
||||||
|
}) async {
|
||||||
|
final url =
|
||||||
|
'${AppLink.googleMapsLink}directions/json?language=${box.read(BoxName.lang)}&destination=${destination.latitude},${destination.longitude}&origin=${origin.latitude},${origin.longitude}&key=${AK.mapAPIKEY}';
|
||||||
|
|
||||||
|
final response = await _crud.getGoogleApi(link: url, payload: {});
|
||||||
|
|
||||||
|
if (response != null && response['routes'].isNotEmpty) {
|
||||||
|
return response['routes'][0];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawRoute(Map<String, dynamic> routeData, {Color color = Colors.blue}) {
|
||||||
|
final pointsString = routeData["overview_polyline"]["points"];
|
||||||
|
final points = decodePolyline(pointsString)
|
||||||
|
.map((p) => LatLng(p[0].toDouble(), p[1].toDouble()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final polyline = Polyline(
|
||||||
|
polylineId: PolylineId(routeData["summary"] ?? DateTime.now().toString()),
|
||||||
|
points: points,
|
||||||
|
width: 8,
|
||||||
|
color: color,
|
||||||
|
);
|
||||||
|
|
||||||
|
polylines.add(polyline);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateCarMarker(LatLng position, double heading) {
|
||||||
|
markers.removeWhere((m) => m.markerId.value == 'MyLocation');
|
||||||
|
markers.add(
|
||||||
|
Marker(
|
||||||
|
markerId: MarkerId('MyLocation'.tr),
|
||||||
|
position: position,
|
||||||
|
icon: carIcon,
|
||||||
|
rotation: heading,
|
||||||
|
anchor: const Offset(0.5, 0.5),
|
||||||
|
flat: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setInitialMarkers(
|
||||||
|
LatLng passengerLocation, LatLng passengerDestination) {
|
||||||
|
markers.clear();
|
||||||
|
markers.add(Marker(
|
||||||
|
markerId: const MarkerId('passengerLocation'),
|
||||||
|
position: passengerLocation,
|
||||||
|
icon: passengerIcon,
|
||||||
|
));
|
||||||
|
markers.add(Marker(
|
||||||
|
markerId: const MarkerId('passengerDestination'),
|
||||||
|
position: passengerDestination,
|
||||||
|
icon: endIcon,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearRoutes() {
|
||||||
|
polylines.clear();
|
||||||
|
currentInstruction.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
import 'dart:math' as math;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -10,6 +11,7 @@ import 'package:sefer_driver/constant/colors.dart';
|
|||||||
// استخدام نفس مسارات الاستيراد التي قدمتها
|
// استخدام نفس مسارات الاستيراد التي قدمتها
|
||||||
import '../../../constant/api_key.dart';
|
import '../../../constant/api_key.dart';
|
||||||
import '../../../constant/links.dart';
|
import '../../../constant/links.dart';
|
||||||
|
import '../../../print.dart';
|
||||||
import '../../functions/crud.dart';
|
import '../../functions/crud.dart';
|
||||||
import '../../functions/tts.dart';
|
import '../../functions/tts.dart';
|
||||||
|
|
||||||
@@ -525,45 +527,127 @@ class NavigationController extends GetxController {
|
|||||||
String _parseInstruction(String html) =>
|
String _parseInstruction(String html) =>
|
||||||
html.replaceAll(RegExp(r'<[^>]*>'), ' ');
|
html.replaceAll(RegExp(r'<[^>]*>'), ' ');
|
||||||
|
|
||||||
|
double _haversineKm(double lat1, double lon1, double lat2, double lon2) {
|
||||||
|
const R = 6371.0; // km
|
||||||
|
final dLat = (lat2 - lat1) * math.pi / 180.0;
|
||||||
|
final dLon = (lon2 - lon1) * math.pi / 180.0;
|
||||||
|
final a = math.sin(dLat / 2) * math.sin(dLat / 2) +
|
||||||
|
math.cos(lat1 * math.pi / 180.0) *
|
||||||
|
math.cos(lat2 * math.pi / 180.0) *
|
||||||
|
math.sin(dLon / 2) *
|
||||||
|
math.sin(dLon / 2);
|
||||||
|
final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
|
||||||
|
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 {
|
Future<void> getPlaces() async {
|
||||||
if (placeDestinationController.text.trim().isEmpty) {
|
final q = placeDestinationController.text.trim();
|
||||||
|
if (q.isEmpty) {
|
||||||
placesDestination = [];
|
placesDestination = [];
|
||||||
update();
|
update();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (myLocation == null) {
|
|
||||||
Get.snackbar('انتظر', 'جاري تحديد موقعك الحالي...');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final query = placeDestinationController.text.trim();
|
|
||||||
final lat = myLocation!.latitude;
|
final lat = myLocation!.latitude;
|
||||||
final lng = myLocation!.longitude;
|
final lng = myLocation!.longitude;
|
||||||
const double range = 2.2;
|
|
||||||
final lat_min = lat - range,
|
// نصف قطر البحث بالكيلومتر (عدّل حسب رغبتك)
|
||||||
lat_max = lat + range,
|
const radiusKm = 200.0;
|
||||||
lng_min = lng - range,
|
|
||||||
lng_max = lng + range;
|
// حساب الباوند الصحيح (درجات، وليس 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 {
|
try {
|
||||||
final response = await CRUD().post(
|
final response = await CRUD().post(
|
||||||
link: AppLink.getPlacesSyria,
|
link: AppLink.getPlacesSyria,
|
||||||
payload: {
|
payload: {
|
||||||
'query': query,
|
'query': q,
|
||||||
'lat_min': lat_min.toString(),
|
'lat_min': latMin.toString(),
|
||||||
'lat_max': lat_max.toString(),
|
'lat_max': latMax.toString(),
|
||||||
'lng_min': lng_min.toString(),
|
'lng_min': lngMin.toString(),
|
||||||
'lng_max': lng_max.toString(),
|
'lng_max': lngMax.toString(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (response != 'failure') {
|
|
||||||
placesDestination = response['message'] ?? [];
|
// يدعم شكلي استجابة: إما {"...","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 {
|
} else {
|
||||||
placesDestination = [];
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
return da.compareTo(db);
|
||||||
|
});
|
||||||
|
|
||||||
|
// خذ أول 10–15 للعرض (اختياري)، أو اعرض الكل
|
||||||
|
placesDestination = list.take(15).toList();
|
||||||
|
Log.print('placesDestination: $placesDestination');
|
||||||
|
update();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Exception in getPlaces: $e');
|
print('Exception in getPlaces: $e');
|
||||||
} finally {
|
|
||||||
update();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:local_auth/local_auth.dart';
|
import 'package:local_auth/local_auth.dart';
|
||||||
import 'package:sefer_driver/constant/box_name.dart';
|
import 'package:sefer_driver/constant/box_name.dart';
|
||||||
|
import 'package:sefer_driver/controller/payment/smsPaymnet/payment_services.dart';
|
||||||
import 'package:sefer_driver/main.dart';
|
import 'package:sefer_driver/main.dart';
|
||||||
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -27,48 +28,48 @@ class PaymobPayout extends GetxController {
|
|||||||
sensitiveTransaction: true,
|
sensitiveTransaction: true,
|
||||||
));
|
));
|
||||||
if (didAuthenticate) {
|
if (didAuthenticate) {
|
||||||
var dec = await CRUD()
|
// var dec = await CRUD()
|
||||||
.postWallet(link: AppLink.paymobPayoutDriverWallet, payload: {
|
// .postWallet(link: AppLink.paymobPayoutDriverWallet, payload: {
|
||||||
"issuer": issuer,
|
// "issuer": issuer,
|
||||||
"method": "wallet",
|
// "method": "wallet",
|
||||||
"amount": amount, //9.0,
|
// "amount": amount, //9.0,
|
||||||
"full_name":
|
// "full_name":
|
||||||
'${box.read(BoxName.nameDriver)} ${box.read(BoxName.lastNameDriver)}',
|
// '${box.read(BoxName.nameDriver)} ${box.read(BoxName.lastNameDriver)}',
|
||||||
"msisdn": msisdn, //"01010101010",
|
// "msisdn": msisdn, //"01010101010",
|
||||||
"bank_transaction_type": "cash_transfer"
|
// "bank_transaction_type": "cash_transfer"
|
||||||
});
|
// });
|
||||||
if (dec['disbursement_status'] == 'successful') {
|
// if (dec['disbursement_status'] == 'successful') {
|
||||||
var paymentToken = await Get.find<CaptainWalletController>()
|
// var paymentToken = await Get.find<CaptainWalletController>()
|
||||||
.generateToken(
|
// .generateToken(
|
||||||
((-1) * (double.parse(dec['amount'].toString())) - payOutFee)
|
// ((-1) * (double.parse(dec['amount'].toString())) - payOutFee)
|
||||||
.toStringAsFixed(0));
|
// .toStringAsFixed(0));
|
||||||
await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
|
// await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
|
||||||
'rideId': DateTime.now().toIso8601String(),
|
// 'rideId': DateTime.now().toIso8601String(),
|
||||||
'amount':
|
// 'amount':
|
||||||
((-1) * (double.parse(dec['amount'].toString())) - payOutFee)
|
// ((-1) * (double.parse(dec['amount'].toString())) - payOutFee)
|
||||||
.toStringAsFixed(0),
|
// .toStringAsFixed(0),
|
||||||
'payment_method': 'payout',
|
// 'payment_method': 'payout',
|
||||||
'passengerID': 'myself',
|
// 'passengerID': 'myself',
|
||||||
'token': paymentToken,
|
// 'token': paymentToken,
|
||||||
'driverID': box.read(BoxName.driverID).toString(),
|
// 'driverID': box.read(BoxName.driverID).toString(),
|
||||||
});
|
// });
|
||||||
await Get.find<CaptainWalletController>()
|
// await Get.find<CaptainWalletController>()
|
||||||
.addSeferWallet('payout fee myself', payOutFee.toString());
|
// .addSeferWallet('payout fee myself', payOutFee.toString());
|
||||||
await updatePaymentToPaid(box.read(BoxName.driverID).toString());
|
// await updatePaymentToPaid(box.read(BoxName.driverID).toString());
|
||||||
await sendEmail(
|
// await sendEmail(
|
||||||
box.read(BoxName.driverID).toString(),
|
// box.read(BoxName.driverID).toString(),
|
||||||
amount,
|
// amount,
|
||||||
box.read(BoxName.phoneDriver).toString(),
|
// box.read(BoxName.phoneDriver).toString(),
|
||||||
box.read(BoxName.nameDriver).toString(),
|
// box.read(BoxName.nameDriver).toString(),
|
||||||
'Wallet',
|
// 'Wallet',
|
||||||
box.read(BoxName.emailDriver).toString());
|
// box.read(BoxName.emailDriver).toString());
|
||||||
|
|
||||||
mySnackbarSuccess('${'Transaction successful'.tr} ${dec['amount']}');
|
// mySnackbarSuccess('${'Transaction successful'.tr} ${dec['amount']}');
|
||||||
|
|
||||||
Get.find<CaptainWalletController>().refreshCaptainWallet();
|
// Get.find<CaptainWalletController>().refreshCaptainWallet();
|
||||||
} else if (dec['disbursement_status'] == 'failed') {
|
// } else if (dec['disbursement_status'] == 'failed') {
|
||||||
mySnackeBarError('Transaction failed'.tr);
|
// mySnackeBarError('Transaction failed'.tr);
|
||||||
}
|
// }
|
||||||
} else {
|
} else {
|
||||||
MyDialog().getDialog('Authentication failed'.tr, ''.tr, () {
|
MyDialog().getDialog('Authentication failed'.tr, ''.tr, () {
|
||||||
Get.back();
|
Get.back();
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class SplashScreenController extends GetxController
|
|||||||
box.read(BoxName.onBoarding) == null
|
box.read(BoxName.onBoarding) == null
|
||||||
? Get.off(() => OnBoardingPage())
|
? Get.off(() => OnBoardingPage())
|
||||||
: box.read(BoxName.phoneDriver) != null &&
|
: box.read(BoxName.phoneDriver) != null &&
|
||||||
box.read(BoxName.phoneVerified) == '1'
|
box.read(BoxName.phoneVerified).toString() == '1'
|
||||||
? await Get.put(LoginDriverController())
|
? await Get.put(LoginDriverController())
|
||||||
.loginWithGoogleCredential(
|
.loginWithGoogleCredential(
|
||||||
box.read(BoxName.driverID).toString(),
|
box.read(BoxName.driverID).toString(),
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ class MyTranslation extends Translations {
|
|||||||
"نأسف لإعلامك بأن سائقًا آخر قد قبل هذا الطلب.",
|
"نأسف لإعلامك بأن سائقًا آخر قد قبل هذا الطلب.",
|
||||||
"Driver Applied the Ride for You": "السائق قدم الطلب لك",
|
"Driver Applied the Ride for You": "السائق قدم الطلب لك",
|
||||||
"Applied": "تم التقديم",
|
"Applied": "تم التقديم",
|
||||||
|
'Pay by Sham Cash': 'الدفع عبر شام كاش',
|
||||||
|
'Pay with Debit Card': 'الدفع ببطاقة الخصم',
|
||||||
"Please go to Car Driver": "يرجى الذهاب إلى سائق السيارة",
|
"Please go to Car Driver": "يرجى الذهاب إلى سائق السيارة",
|
||||||
"Ok I will go now.": "حسنًا، سأذهب الآن.",
|
"Ok I will go now.": "حسنًا، سأذهب الآن.",
|
||||||
"Accepted Ride": "تم قبول الرحلة",
|
"Accepted Ride": "تم قبول الرحلة",
|
||||||
@@ -84,6 +86,8 @@ class MyTranslation extends Translations {
|
|||||||
"Password must be at least 6 characters":
|
"Password must be at least 6 characters":
|
||||||
"يجب أن تتكون كلمة المرور من 6 أحرف على الأقل",
|
"يجب أن تتكون كلمة المرور من 6 أحرف على الأقل",
|
||||||
"Create Account": "إنشاء حساب",
|
"Create Account": "إنشاء حساب",
|
||||||
|
'Pay by MTN Wallet': 'الدفع عبر محفظة MTN',
|
||||||
|
'Pay by Syriatel Wallet': 'الدفع عبر محفظة سيريتل',
|
||||||
"Login": "تسجيل الدخول",
|
"Login": "تسجيل الدخول",
|
||||||
"Back to other sign-in options": "العودة إلى خيارات التسجيل الأخرى",
|
"Back to other sign-in options": "العودة إلى خيارات التسجيل الأخرى",
|
||||||
"Driver Agreement": "اتفاقية السائق",
|
"Driver Agreement": "اتفاقية السائق",
|
||||||
@@ -93,7 +97,11 @@ class MyTranslation extends Translations {
|
|||||||
" and acknowledge our Privacy Policy.":
|
" and acknowledge our Privacy Policy.":
|
||||||
" والإقرار بسياسة الخصوصية الخاصة بنا.",
|
" والإقرار بسياسة الخصوصية الخاصة بنا.",
|
||||||
"I Agree": "أنا أوافق",
|
"I Agree": "أنا أوافق",
|
||||||
"Continue": "متابعة",
|
"Continue": "متابعة", "Customer not found": "العميل غير موجود",
|
||||||
|
"Wallet is blocked": "المحفظة محظورة",
|
||||||
|
"Customer phone is not active": "هاتف العميل غير نشط",
|
||||||
|
"Balance not enough": "الرصيد غير كافٍ",
|
||||||
|
"Balance limit exceeded": "تم تجاوز حد الرصيد",
|
||||||
"Privacy Policy": "سياسة الخصوصية",
|
"Privacy Policy": "سياسة الخصوصية",
|
||||||
"Location Access Required": "مطلوب الوصول إلى الموقع",
|
"Location Access Required": "مطلوب الوصول إلى الموقع",
|
||||||
"We need access to your location to match you with nearby passengers and provide accurate navigation.":
|
"We need access to your location to match you with nearby passengers and provide accurate navigation.":
|
||||||
@@ -383,6 +391,7 @@ Raih Gai: For same-day return trips longer than 50km.
|
|||||||
"Driver's Personal Information": "المعلومات الشخصية للسائق",
|
"Driver's Personal Information": "المعلومات الشخصية للسائق",
|
||||||
"First Name": "الاسم الأول",
|
"First Name": "الاسم الأول",
|
||||||
"Last Name": "اسم العائلة",
|
"Last Name": "اسم العائلة",
|
||||||
|
'You have gift 30000 SYP': 'لديك هدية 30000 ليرة سورية',
|
||||||
"National ID Number": "الرقم الوطني",
|
"National ID Number": "الرقم الوطني",
|
||||||
"License Expiry Date": "تاريخ انتهاء الرخصة",
|
"License Expiry Date": "تاريخ انتهاء الرخصة",
|
||||||
"YYYY-MM-DD": "YYYY-MM-DD",
|
"YYYY-MM-DD": "YYYY-MM-DD",
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:sefer_driver/controller/functions/crud.dart';
|
||||||
|
|
||||||
|
class PayoutService {
|
||||||
|
final String _baseUrl =
|
||||||
|
"https://walletintaleq.intaleq.xyz/v1/main/sms_webhook";
|
||||||
|
static const double payoutFee = 5000.0; // عمولة السحب الثابتة
|
||||||
|
|
||||||
|
/// دالة لإنشاء طلب سحب جديد على السيرفر
|
||||||
|
///
|
||||||
|
/// تعيد رسالة النجاح من السيرفر، أو رسالة خطأ في حال الفشل.
|
||||||
|
Future<String?> requestPayout({
|
||||||
|
required String driverId,
|
||||||
|
walletType,
|
||||||
|
payoutPhoneNumber,
|
||||||
|
required double amount,
|
||||||
|
}) async {
|
||||||
|
final url = ("$_baseUrl/request_payout.php");
|
||||||
|
try {
|
||||||
|
// هنا يمكنك إضافة هيدرز المصادقة (JWT) بنفس طريقتك المعتادة
|
||||||
|
final response = await CRUD().postWallet(link: url, payload: {
|
||||||
|
'driverId': driverId,
|
||||||
|
'amount': amount.toString(),
|
||||||
|
'phone': payoutPhoneNumber.toString(),
|
||||||
|
'wallet_type': walletType.toString(),
|
||||||
|
}).timeout(const Duration(seconds: 20));
|
||||||
|
|
||||||
|
if (response != 'failure') {
|
||||||
|
final data = (response);
|
||||||
|
if (data['status'] == 'success') {
|
||||||
|
debugPrint("Payout request successful: ${data['message']}");
|
||||||
|
return data['message']; // إرجاع رسالة النجاح
|
||||||
|
} else {
|
||||||
|
debugPrint("Payout request failed: ${data['message']}");
|
||||||
|
return "فشل الطلب: ${data['message']}"; // إرجاع رسالة الخطأ من السيرفر
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "خطأ في الاتصال بالسيرفر: ${response.statusCode}";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Exception during payout request: $e");
|
||||||
|
return "حدث خطأ غير متوقع. يرجى المحاولة مرة أخرى.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
397
lib/controller/payment/smsPaymnet/payment_services.dart
Normal file
397
lib/controller/payment/smsPaymnet/payment_services.dart
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
// لإضافة هذه الحزمة، قم بتشغيل الأمر التالي في الـ Terminal
|
||||||
|
// flutter pub add intl
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:sefer_driver/constant/links.dart';
|
||||||
|
import 'package:sefer_driver/controller/functions/crud.dart';
|
||||||
|
|
||||||
|
import '../../../constant/box_name.dart';
|
||||||
|
import '../../../main.dart';
|
||||||
|
|
||||||
|
/// خدمة لإدارة عمليات الدفع المتعلقة بنظام الدفع عبر الرسائل القصيرة
|
||||||
|
class PaymentService {
|
||||||
|
final String _baseUrl = "${AppLink.seferPaymentServer}/sms_webhook";
|
||||||
|
|
||||||
|
Future<String?> createInvoice({
|
||||||
|
required String userPhone,
|
||||||
|
required double amount,
|
||||||
|
}) async {
|
||||||
|
final url = "$_baseUrl/create_invoice.php";
|
||||||
|
try {
|
||||||
|
final response = await CRUD().postWallet(
|
||||||
|
link: url,
|
||||||
|
payload: {
|
||||||
|
'user_phone': userPhone.toString(),
|
||||||
|
'driverID': box.read(BoxName.driverID),
|
||||||
|
'amount': amount.toString(),
|
||||||
|
},
|
||||||
|
).timeout(const Duration(seconds: 15)); // إضافة مهلة للطلب
|
||||||
|
|
||||||
|
if (response != 'failure') {
|
||||||
|
final data = (response);
|
||||||
|
if (data['status'] == 'success' && data['invoice_number'] != null) {
|
||||||
|
debugPrint(
|
||||||
|
"تم إنشاء الفاتورة بنجاح. الرقم: ${data['invoice_number']}");
|
||||||
|
return data['invoice_number'].toString();
|
||||||
|
} else {
|
||||||
|
debugPrint("فشل في إنشاء الفاتورة من السيرفر: ${data['message']}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugPrint("خطأ في السيرفر عند إنشاء الفاتورة: ${response.statusCode}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("حدث استثناء عند إنشاء الفاتورة: $e");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// دالة للتحقق من حالة فاتورة واحدة
|
||||||
|
Future<bool> checkInvoiceStatus(String invoiceNumber) async {
|
||||||
|
final url = "$_baseUrl/check_invoice_status.php";
|
||||||
|
try {
|
||||||
|
final response = await CRUD().postWallet(link: url, payload: {
|
||||||
|
'invoice_number': invoiceNumber,
|
||||||
|
}).timeout(const Duration(seconds: 10)); // مهلة للشبكة
|
||||||
|
|
||||||
|
if (response != 'failure') {
|
||||||
|
final data = (response);
|
||||||
|
return data['status'] == 'success' &&
|
||||||
|
data['invoice_status'] == 'completed';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("خطأ أثناء التحقق من الفاتورة: $e");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PaymentStatus {
|
||||||
|
creatingInvoice,
|
||||||
|
waitingForPayment,
|
||||||
|
paymentSuccess,
|
||||||
|
paymentTimeout,
|
||||||
|
paymentError
|
||||||
|
}
|
||||||
|
|
||||||
|
class PaymentScreenSmsProvider extends StatefulWidget {
|
||||||
|
final double amount;
|
||||||
|
final String providerName;
|
||||||
|
final String providerLogo;
|
||||||
|
final String paymentPhoneNumber;
|
||||||
|
|
||||||
|
const PaymentScreenSmsProvider({
|
||||||
|
super.key,
|
||||||
|
required this.amount,
|
||||||
|
this.providerName = 'شام كاش',
|
||||||
|
this.providerLogo = 'assets/images/shamCash.png',
|
||||||
|
this.paymentPhoneNumber = '963942542053',
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
_PaymentScreenSmsProviderState createState() =>
|
||||||
|
_PaymentScreenSmsProviderState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
|
||||||
|
final PaymentService _paymentService = PaymentService();
|
||||||
|
Timer? _pollingTimer;
|
||||||
|
PaymentStatus _status = PaymentStatus.creatingInvoice;
|
||||||
|
String? _invoiceNumber;
|
||||||
|
final String phone = box.read(BoxName.phoneWallet);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_createAndPollInvoice();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_pollingTimer?.cancel(); // مهم جداً: إلغاء المؤقت عند الخروج من الشاشة
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _createAndPollInvoice() async {
|
||||||
|
setState(() => _status = PaymentStatus.creatingInvoice);
|
||||||
|
|
||||||
|
final invoiceNumber = await _paymentService.createInvoice(
|
||||||
|
userPhone: phone,
|
||||||
|
amount: widget.amount,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (invoiceNumber != null && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_invoiceNumber = invoiceNumber;
|
||||||
|
_status = PaymentStatus.waitingForPayment;
|
||||||
|
});
|
||||||
|
_startPolling(invoiceNumber);
|
||||||
|
} else if (mounted) {
|
||||||
|
setState(() => _status = PaymentStatus.paymentError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startPolling(String invoiceNumber) {
|
||||||
|
const timeoutDuration = Duration(minutes: 3);
|
||||||
|
var elapsed = Duration.zero;
|
||||||
|
|
||||||
|
_pollingTimer = Timer.periodic(const Duration(seconds: 5), (timer) async {
|
||||||
|
elapsed += const Duration(seconds: 5);
|
||||||
|
if (elapsed >= timeoutDuration) {
|
||||||
|
timer.cancel();
|
||||||
|
if (mounted) setState(() => _status = PaymentStatus.paymentTimeout);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint("Polling... Checking invoice status for: $invoiceNumber");
|
||||||
|
final isCompleted =
|
||||||
|
await _paymentService.checkInvoiceStatus(invoiceNumber);
|
||||||
|
if (isCompleted && mounted) {
|
||||||
|
timer.cancel();
|
||||||
|
setState(() => _status = PaymentStatus.paymentSuccess);
|
||||||
|
// TODO: تحديث رصيد المستخدم أو تنفيذ الإجراءات اللازمة
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// دالة جديدة لمعالجة محاولة الرجوع للخلف
|
||||||
|
void _onPopInvoked(bool didPop) async {
|
||||||
|
// إذا كان الرجوع قد تم بالفعل (مثلاً من خلال Navigator.pop)، لا تفعل شيئاً
|
||||||
|
if (didPop) return;
|
||||||
|
|
||||||
|
// إذا كان المستخدم ينتظر الدفع، أظهر له حوار التأكيد
|
||||||
|
if (_status == PaymentStatus.waitingForPayment) {
|
||||||
|
final shouldPop = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('هل أنت متأكد؟'),
|
||||||
|
content: const Text('إذا خرجت الآن، سيتم إلغاء عملية الدفع الحالية.'),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('البقاء'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: const Text('الخروج'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// إذا وافق المستخدم على الخروج، قم بإغلاق الشاشة
|
||||||
|
if (shouldPop ?? false) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// استخدام PopScope بدلاً من WillPopScope
|
||||||
|
return PopScope(
|
||||||
|
// منع الرجوع التلقائي فقط في حالة انتظار الدفع
|
||||||
|
canPop: _status != PaymentStatus.waitingForPayment,
|
||||||
|
// استدعاء دالة التحقق عند محاولة الرجوع
|
||||||
|
onPopInvoked: _onPopInvoked,
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(title: Text("الدفع عبر ${widget.providerName}")),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Center(
|
||||||
|
child: _buildContentByStatus(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContentByStatus() {
|
||||||
|
switch (_status) {
|
||||||
|
case PaymentStatus.creatingInvoice:
|
||||||
|
return const Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(),
|
||||||
|
SizedBox(height: 20),
|
||||||
|
Text("جاري إنشاء فاتورة الدفع...", style: TextStyle(fontSize: 16)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
case PaymentStatus.waitingForPayment:
|
||||||
|
return _buildWaitingForPaymentUI();
|
||||||
|
case PaymentStatus.paymentSuccess:
|
||||||
|
return _buildSuccessUI();
|
||||||
|
case PaymentStatus.paymentTimeout:
|
||||||
|
case PaymentStatus.paymentError:
|
||||||
|
return _buildErrorUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildWaitingForPaymentUI() {
|
||||||
|
final currencyFormat = NumberFormat.decimalPattern('ar_SY');
|
||||||
|
final invoiceText = _invoiceNumber ?? '------';
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Image.asset(widget.providerLogo, width: 96),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text("تعليمات الدفع", style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Card(
|
||||||
|
elevation: 1.5,
|
||||||
|
shape:
|
||||||
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_StepTile(number: 1, text: "افتح تطبيق محفظتك الإلكترونية."),
|
||||||
|
_StepTile(number: 2, text: "اختر خدمة تحويل الأموال."),
|
||||||
|
_StepTile(
|
||||||
|
number: 3,
|
||||||
|
text:
|
||||||
|
"أدخل المبلغ المطلوب: ${currencyFormat.format(widget.amount)} ل.س"),
|
||||||
|
_StepTile(number: 4, text: "حوّل إلى الرقم التالي:"),
|
||||||
|
// --- التعديل هنا ---
|
||||||
|
ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: Text(
|
||||||
|
widget.paymentPhoneNumber,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 1.2),
|
||||||
|
),
|
||||||
|
trailing: OutlinedButton.icon(
|
||||||
|
onPressed: () async {
|
||||||
|
await Clipboard.setData(
|
||||||
|
ClipboardData(text: widget.paymentPhoneNumber));
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text("تم نسخ رقم الهاتف")));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.copy, size: 18),
|
||||||
|
label: const Text("نسخ"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// --- نهاية التعديل ---
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_StepTile(
|
||||||
|
number: 5,
|
||||||
|
text: "هام: انسخ رقم القسيمة والصقه في خانة \"البيان\"."),
|
||||||
|
ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: Text(invoiceText,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 1.5)),
|
||||||
|
trailing: OutlinedButton.icon(
|
||||||
|
onPressed: _invoiceNumber == null
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
await Clipboard.setData(
|
||||||
|
ClipboardData(text: invoiceText));
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text("تم نسخ رقم القسيمة")));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.copy, size: 18),
|
||||||
|
label: const Text("نسخ"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
const LinearProgressIndicator(minHeight: 2),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text("بانتظار تأكيد الدفع...",
|
||||||
|
style: TextStyle(color: Colors.grey.shade700)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
const Text("هذه الشاشة ستتحدث تلقائيًا",
|
||||||
|
style: TextStyle(color: Colors.grey)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSuccessUI() {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.check_circle, color: Colors.green, size: 80),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
const Text("تم الدفع بنجاح!",
|
||||||
|
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text("العودة"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildErrorUI() {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.error, color: Colors.red, size: 80),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
_status == PaymentStatus.paymentTimeout
|
||||||
|
? "انتهى الوقت المحدد للدفع"
|
||||||
|
: "حدث خطأ ما",
|
||||||
|
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text("يرجى المحاولة مرة أخرى.", style: TextStyle(fontSize: 16)),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _createAndPollInvoice,
|
||||||
|
child: const Text("المحاولة مرة أخرى"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ويدجت مساعد لعرض خطوات التعليمات بشكل أنيق
|
||||||
|
class _StepTile extends StatelessWidget {
|
||||||
|
final int number;
|
||||||
|
final String text;
|
||||||
|
const _StepTile({required this.number, required this.text});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
leading: CircleAvatar(
|
||||||
|
radius: 12,
|
||||||
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
|
child: Text("$number",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
title: Text(text),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@ class CaptainProfileController extends GetxController {
|
|||||||
await CRUD().post(link: AppLink.updateDriverEmail, payload: payload);
|
await CRUD().post(link: AppLink.updateDriverEmail, payload: payload);
|
||||||
|
|
||||||
if ((res)['status'] == 'success') {
|
if ((res)['status'] == 'success') {
|
||||||
box.write(BoxName.email, emailController.text);
|
box.write(BoxName.emailDriver, emailController.text);
|
||||||
update();
|
update();
|
||||||
Get.back();
|
Get.back();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -144,14 +144,28 @@ void main() async {
|
|||||||
DeviceOrientation.portraitDown,
|
DeviceOrientation.portraitDown,
|
||||||
]);
|
]);
|
||||||
runZonedGuarded<Future<void>>(() async {
|
runZonedGuarded<Future<void>>(() async {
|
||||||
// ... الكود الحالي الموجود في دالة main ...
|
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
}, (error, stack) {
|
}, (error, stack) {
|
||||||
// أي خطأ غير متوقع في التطبيق سيتم التقاطه هنا CRUD.
|
// ==== START: ERROR FILTER ====
|
||||||
|
String errorString = error.toString();
|
||||||
|
|
||||||
|
// Print all errors to the local debug console for development
|
||||||
print("Caught Dart error: $error");
|
print("Caught Dart error: $error");
|
||||||
print(stack);
|
print(stack);
|
||||||
// أرسل الخطأ إلى السيرفر
|
|
||||||
CRUD.addError(error.toString(), stack.toString(), 'main');
|
// 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 ====
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -284,6 +284,9 @@ class RegistrationView extends StatelessWidget {
|
|||||||
|
|
||||||
// STEP 3
|
// STEP 3
|
||||||
Widget _buildDocumentUploadStep(BuildContext ctx, RegistrationController c) {
|
Widget _buildDocumentUploadStep(BuildContext ctx, RegistrationController c) {
|
||||||
|
final String linkUpload =
|
||||||
|
'https://syria.intaleq.xyz/intaleq/auth/syria/uploadImage.php';
|
||||||
|
|
||||||
return GetBuilder<RegistrationController>(
|
return GetBuilder<RegistrationController>(
|
||||||
builder: (ctrl) => SingleChildScrollView(
|
builder: (ctrl) => SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
@@ -296,23 +299,31 @@ class RegistrationView extends StatelessWidget {
|
|||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_buildImagePickerBox(
|
_buildImagePickerBox(
|
||||||
'Driver License (Front)'.tr,
|
'Driver License (Front)'.tr,
|
||||||
ctrl.driverLicenseFrontImage,
|
ctrl.docUrls['driver_license_front'],
|
||||||
() => ctrl.pickImage(ImageType.driverLicenseFront),
|
// () => ctrl.pickImage(ImageType.driverLicenseFront),
|
||||||
|
|
||||||
|
() async =>
|
||||||
|
await ctrl.choosImage(linkUpload, 'driver_license_front'),
|
||||||
),
|
),
|
||||||
_buildImagePickerBox(
|
_buildImagePickerBox(
|
||||||
'Driver License (Back)'.tr,
|
'Driver License (Back)'.tr,
|
||||||
ctrl.driverLicenseBackImage,
|
ctrl.docUrls['driver_license_back'],
|
||||||
() => ctrl.pickImage(ImageType.driverLicenseBack),
|
() async =>
|
||||||
|
await ctrl.choosImage(linkUpload, 'driver_license_back'),
|
||||||
|
// () => ctrl.pickImage(ImageType.driverLicenseBack),
|
||||||
),
|
),
|
||||||
_buildImagePickerBox(
|
_buildImagePickerBox(
|
||||||
'Car Registration (Front)'.tr,
|
'Car Registration (Front)'.tr,
|
||||||
ctrl.carLicenseFrontImage,
|
ctrl.docUrls['car_license_front'],
|
||||||
() => ctrl.pickImage(ImageType.carLicenseFront),
|
() async =>
|
||||||
|
await ctrl.choosImage(linkUpload, 'car_license_front'),
|
||||||
|
// () => ctrl.pickImage(ImageType.carLicenseFront),
|
||||||
),
|
),
|
||||||
_buildImagePickerBox(
|
_buildImagePickerBox(
|
||||||
'Car Registration (Back)'.tr,
|
'Car Registration (Back)'.tr,
|
||||||
ctrl.carLicenseBackImage,
|
ctrl.docUrls['car_license_back'],
|
||||||
() => ctrl.pickImage(ImageType.carLicenseBack),
|
() async => await ctrl.choosImage(linkUpload, 'car_license_back'),
|
||||||
|
// () => ctrl.pickImage(ImageType.carLicenseBack),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -329,7 +340,7 @@ class RegistrationView extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildImagePickerBox(String title, File? img, VoidCallback onTap) {
|
Widget _buildImagePickerBox(String title, String? img, VoidCallback onTap) {
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
@@ -337,8 +348,24 @@ class RegistrationView extends StatelessWidget {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 150,
|
height: 150,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: img != null
|
child: (img != null && img.isNotEmpty)
|
||||||
? Image.file(img, fit: BoxFit.fill)
|
? Image.network(
|
||||||
|
img,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.broken_image, size: 40, color: Colors.red),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text('Image not available',
|
||||||
|
style: TextStyle(color: Colors.red[700])),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
: Column(
|
: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:sefer_driver/constant/box_name.dart';
|
import 'package:sefer_driver/constant/box_name.dart';
|
||||||
import 'package:sefer_driver/controller/firebase/local_notification.dart';
|
import 'package:sefer_driver/controller/firebase/local_notification.dart';
|
||||||
|
import 'package:sefer_driver/controller/functions/network/net_guard.dart';
|
||||||
|
import 'package:sefer_driver/controller/functions/sms_egypt_controller.dart';
|
||||||
import 'package:sefer_driver/main.dart';
|
import 'package:sefer_driver/main.dart';
|
||||||
import 'package:sefer_driver/views/auth/captin/login_captin.dart';
|
import 'package:sefer_driver/views/auth/captin/login_captin.dart';
|
||||||
import 'package:sefer_driver/views/home/Captin/driver_map_page.dart';
|
import 'package:sefer_driver/views/home/Captin/driver_map_page.dart';
|
||||||
@@ -14,10 +16,12 @@ import '../../../../../constant/colors.dart';
|
|||||||
import '../../../../../constant/links.dart';
|
import '../../../../../constant/links.dart';
|
||||||
import '../../../../../controller/firebase/firbase_messge.dart';
|
import '../../../../../controller/firebase/firbase_messge.dart';
|
||||||
import '../../../../../controller/functions/crud.dart';
|
import '../../../../../controller/functions/crud.dart';
|
||||||
|
import '../../../../../controller/functions/encrypt_decrypt.dart';
|
||||||
import '../../../../../controller/home/captin/order_request_controller.dart';
|
import '../../../../../controller/home/captin/order_request_controller.dart';
|
||||||
import '../../../../../controller/home/navigation/navigation_view.dart';
|
import '../../../../../controller/home/navigation/navigation_view.dart';
|
||||||
import '../../../../Rate/ride_calculate_driver.dart';
|
import '../../../../Rate/ride_calculate_driver.dart';
|
||||||
import '../../../../auth/syria/registration_view.dart';
|
import '../../../../auth/syria/registration_view.dart';
|
||||||
|
import '../../../../widgets/error_snakbar.dart';
|
||||||
|
|
||||||
GetBuilder<HomeCaptainController> leftMainMenuCaptainIcons() {
|
GetBuilder<HomeCaptainController> leftMainMenuCaptainIcons() {
|
||||||
final firebaseMessagesController =
|
final firebaseMessagesController =
|
||||||
@@ -181,10 +185,8 @@ GetBuilder<HomeCaptainController> leftMainMenuCaptainIcons() {
|
|||||||
// child: Builder(builder: (context) {
|
// child: Builder(builder: (context) {
|
||||||
// return IconButton(
|
// return IconButton(
|
||||||
// onPressed: () async {
|
// onPressed: () async {
|
||||||
// box.remove(BoxName.agreeTerms);
|
// var finger = await storage.read(key: BoxName.fingerPrint);
|
||||||
// Get.to(() => const NavigationView());
|
//
|
||||||
|
|
||||||
// // box.write(BoxName.statusDriverLocation, 'off');
|
|
||||||
// },
|
// },
|
||||||
// icon: const Icon(
|
// icon: const Icon(
|
||||||
// FontAwesome5.grin_tears,
|
// FontAwesome5.grin_tears,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:just_audio/just_audio.dart';
|
|||||||
import 'package:sefer_driver/constant/api_key.dart';
|
import 'package:sefer_driver/constant/api_key.dart';
|
||||||
import '../../../../constant/box_name.dart';
|
import '../../../../constant/box_name.dart';
|
||||||
import '../../../../constant/links.dart';
|
import '../../../../constant/links.dart';
|
||||||
|
import '../../../../controller/firebase/firbase_messge.dart';
|
||||||
import '../../../../controller/firebase/local_notification.dart';
|
import '../../../../controller/firebase/local_notification.dart';
|
||||||
import '../../../../controller/functions/crud.dart';
|
import '../../../../controller/functions/crud.dart';
|
||||||
import '../../../../main.dart';
|
import '../../../../main.dart';
|
||||||
@@ -218,16 +219,22 @@ class _OrderOverlayState extends State<OrderOverlay>
|
|||||||
'status': 'Apply',
|
'status': 'Apply',
|
||||||
'driver_id': box.read(BoxName.driverID),
|
'driver_id': box.read(BoxName.driverID),
|
||||||
});
|
});
|
||||||
if (AppLink.endPoint != AppLink.seferCairoServer) {
|
List<String> bodyToPassenger = [
|
||||||
CRUD().post(
|
_getData(6).toString(),
|
||||||
link: "${AppLink.endPoint}/ride/rides/updateStausFromSpeed.php",
|
_getData(8).toString(),
|
||||||
payload: {
|
_getData(9).toString(),
|
||||||
'id': orderData!.orderId,
|
];
|
||||||
'rideTimeStart': DateTime.now().toString(),
|
final fmc = Get.isRegistered<FirebaseMessagesController>()
|
||||||
'status': 'Apply',
|
? Get.find<FirebaseMessagesController>()
|
||||||
'driver_id': box.read(BoxName.driverID),
|
: Get.put(FirebaseMessagesController());
|
||||||
});
|
|
||||||
}
|
fmc.sendNotificationToDriverMAP(
|
||||||
|
"Accepted Ride",
|
||||||
|
'your ride is Accepted'.tr,
|
||||||
|
_getData(9).toString(),
|
||||||
|
bodyToPassenger,
|
||||||
|
'start.wav',
|
||||||
|
);
|
||||||
final payload = {
|
final payload = {
|
||||||
// بيانات أساسية
|
// بيانات أساسية
|
||||||
'driver_id': driverId,
|
'driver_id': driverId,
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ class _OrderRequestPageState extends State<OrderRequestPage> {
|
|||||||
: Get.put(FirebaseMessagesController());
|
: Get.put(FirebaseMessagesController());
|
||||||
|
|
||||||
fmc.sendNotificationToDriverMAP(
|
fmc.sendNotificationToDriverMAP(
|
||||||
"Accepted Ride".tr,
|
"Accepted Ride",
|
||||||
'your ride is Accepted'.tr,
|
'your ride is Accepted'.tr,
|
||||||
controller.myList[9].toString(),
|
controller.myList[9].toString(),
|
||||||
bodyToPassenger,
|
bodyToPassenger,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sefer_driver/views/home/my_wallet/pay_out_screen.dart';
|
||||||
|
|
||||||
import '../../../constant/box_name.dart';
|
import '../../../constant/box_name.dart';
|
||||||
import '../../../constant/colors.dart';
|
import '../../../constant/colors.dart';
|
||||||
@@ -164,7 +165,7 @@ class CardSeferWalletDriver extends StatelessWidget {
|
|||||||
|
|
||||||
void _showCashOutDialog(
|
void _showCashOutDialog(
|
||||||
BuildContext context, CaptainWalletController captainWalletController) {
|
BuildContext context, CaptainWalletController captainWalletController) {
|
||||||
double minAmount = 20.0; // الحد الأدنى للسحب
|
double minAmount = 20000.0; // الحد الأدنى للسحب
|
||||||
if (double.parse(captainWalletController.totalAmountVisa) >= minAmount) {
|
if (double.parse(captainWalletController.totalAmountVisa) >= minAmount) {
|
||||||
Get.defaultDialog(
|
Get.defaultDialog(
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
@@ -208,16 +209,29 @@ class CardSeferWalletDriver extends StatelessWidget {
|
|||||||
confirm: MyElevatedButton(
|
confirm: MyElevatedButton(
|
||||||
title: 'تأكيد'.tr,
|
title: 'تأكيد'.tr,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
box.write(
|
||||||
|
BoxName.phoneWallet, captainWalletController.phoneWallet);
|
||||||
|
box.write(BoxName.walletType,
|
||||||
|
Get.find<SyrianPayoutController>().dropdownValue.toString());
|
||||||
if (captainWalletController.formKey.currentState!.validate()) {
|
if (captainWalletController.formKey.currentState!.validate()) {
|
||||||
Get.back();
|
Get.back();
|
||||||
String amountAfterFee =
|
Get.to(() => PayoutScreen(
|
||||||
(double.parse(captainWalletController.totalAmountVisa) - 5)
|
amountToWithdraw:
|
||||||
.toStringAsFixed(0);
|
double.parse(captainWalletController.totalAmountVisa),
|
||||||
await Get.put(PaymobPayout()).payToWalletDriverAll(
|
payoutPhoneNumber:
|
||||||
amountAfterFee,
|
captainWalletController.phoneWallet.text.toString(),
|
||||||
Get.find<SyrianPayoutController>().dropdownValue.toString(),
|
walletType: Get.find<SyrianPayoutController>()
|
||||||
captainWalletController.phoneWallet.text.toString(),
|
.dropdownValue
|
||||||
);
|
.toString(),
|
||||||
|
));
|
||||||
|
// String amountAfterFee =
|
||||||
|
// (double.parse(captainWalletController.totalAmountVisa) - 5)
|
||||||
|
// .toStringAsFixed(0);
|
||||||
|
// await Get.put(PaymobPayout()).payToWalletDriverAll(
|
||||||
|
// amountAfterFee,
|
||||||
|
// Get.find<SyrianPayoutController>().dropdownValue.toString(),
|
||||||
|
// captainWalletController.phoneWallet.text.toString(),
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
kolor: AppColor.greenColor,
|
kolor: AppColor.greenColor,
|
||||||
@@ -274,7 +288,7 @@ class MyDropDownSyria extends StatelessWidget {
|
|||||||
onChanged: (String? newValue) {
|
onChanged: (String? newValue) {
|
||||||
controller.changeValue(newValue);
|
controller.changeValue(newValue);
|
||||||
},
|
},
|
||||||
items: <String>['syriatel', 'mtn']
|
items: <String>['Syriatel', 'Cash Mobile', 'Sham Cash']
|
||||||
.map<DropdownMenuItem<String>>((String value) {
|
.map<DropdownMenuItem<String>>((String value) {
|
||||||
return DropdownMenuItem<String>(
|
return DropdownMenuItem<String>(
|
||||||
value: value,
|
value: value,
|
||||||
@@ -288,7 +302,7 @@ class MyDropDownSyria extends StatelessWidget {
|
|||||||
|
|
||||||
// هذا المتحكم ضروري لعمل القائمة المنسدلة
|
// هذا المتحكم ضروري لعمل القائمة المنسدلة
|
||||||
class SyrianPayoutController extends GetxController {
|
class SyrianPayoutController extends GetxController {
|
||||||
String dropdownValue = 'syriatel';
|
String dropdownValue = 'Syriatel';
|
||||||
|
|
||||||
void changeValue(String? newValue) {
|
void changeValue(String? newValue) {
|
||||||
if (newValue != null) {
|
if (newValue != null) {
|
||||||
|
|||||||
196
lib/views/home/my_wallet/pay_out_screen.dart
Normal file
196
lib/views/home/my_wallet/pay_out_screen.dart
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:local_auth/local_auth.dart';
|
||||||
|
import 'package:sefer_driver/constant/box_name.dart';
|
||||||
|
import 'package:sefer_driver/main.dart';
|
||||||
|
|
||||||
|
import '../../../controller/payment/smsPaymnet/pay_out_syria_controller.dart';
|
||||||
|
|
||||||
|
class PayoutScreen extends StatefulWidget {
|
||||||
|
// استقبال كل البيانات المطلوبة جاهزة
|
||||||
|
final double amountToWithdraw;
|
||||||
|
final String payoutPhoneNumber;
|
||||||
|
final String walletType;
|
||||||
|
|
||||||
|
const PayoutScreen({
|
||||||
|
super.key,
|
||||||
|
required this.amountToWithdraw,
|
||||||
|
required this.payoutPhoneNumber,
|
||||||
|
required this.walletType,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
_PayoutScreenState createState() => _PayoutScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PayoutScreenState extends State<PayoutScreen> {
|
||||||
|
final _payoutService = PayoutService();
|
||||||
|
final _localAuth = LocalAuthentication();
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
|
Future<void> _handlePayoutRequest() async {
|
||||||
|
try {
|
||||||
|
// 1. طلب المصادقة البيومترية
|
||||||
|
bool didAuthenticate = await _localAuth.authenticate(
|
||||||
|
localizedReason: 'استخدم بصمة الإصبع لتأكيد عملية السحب',
|
||||||
|
options: const AuthenticationOptions(
|
||||||
|
biometricOnly: true,
|
||||||
|
sensitiveTransaction: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (didAuthenticate && mounted) {
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
|
// 2. إرسال الطلب إلى السيرفر بالبيانات الجاهزة
|
||||||
|
final result = await _payoutService.requestPayout(
|
||||||
|
driverId:
|
||||||
|
box.read(BoxName.driverID).toString(), // استبدله بـ box.read
|
||||||
|
amount: widget.amountToWithdraw,
|
||||||
|
payoutPhoneNumber: widget.payoutPhoneNumber,
|
||||||
|
walletType: widget.walletType,
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
|
||||||
|
if (result != null && result.contains("successfully")) {
|
||||||
|
// 3. عرض رسالة النجاح النهائية
|
||||||
|
_showSuccessDialog();
|
||||||
|
} else {
|
||||||
|
_showErrorDialog(result ?? "حدث خطأ غير معروف.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
_showErrorDialog("جهازك لا يدعم المصادقة البيومترية أو لم يتم إعدادها.");
|
||||||
|
debugPrint("Biometric error: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// حساب المبلغ الإجمالي المخصوم
|
||||||
|
final totalDeducted = widget.amountToWithdraw + PayoutService.payoutFee;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text("تأكيد سحب الأموال")),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.wallet, size: 64, color: Colors.blue),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
"تأكيد تفاصيل عملية السحب",
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildSummaryCard(totalDeducted),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
_isLoading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: ElevatedButton.icon(
|
||||||
|
onPressed: _handlePayoutRequest,
|
||||||
|
icon: const Icon(Icons.fingerprint),
|
||||||
|
label: const Text("تأكيد السحب بالبصمة"),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
textStyle: const TextStyle(
|
||||||
|
fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSummaryCard(double totalDeducted) {
|
||||||
|
return Card(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_summaryRow("المبلغ المسحوب:",
|
||||||
|
"${widget.amountToWithdraw.toStringAsFixed(2)} ل.س"),
|
||||||
|
const Divider(),
|
||||||
|
_summaryRow("عمولة السحب:",
|
||||||
|
"${PayoutService.payoutFee.toStringAsFixed(2)} ل.س"),
|
||||||
|
const Divider(thickness: 1.5),
|
||||||
|
_summaryRow(
|
||||||
|
"الإجمالي المخصوم من رصيدك:",
|
||||||
|
"${totalDeducted.toStringAsFixed(2)} ل.س",
|
||||||
|
isTotal: true,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_summaryRow("سيتم التحويل إلى هاتف:", widget.payoutPhoneNumber),
|
||||||
|
_summaryRow("عبر محفظة:", widget.walletType),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _summaryRow(String title, String value, {bool isTotal = false}) {
|
||||||
|
final titleStyle = TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: isTotal ? Theme.of(context).primaryColor : Colors.black87,
|
||||||
|
fontWeight: isTotal ? FontWeight.bold : FontWeight.normal,
|
||||||
|
);
|
||||||
|
final valueStyle = titleStyle.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(title, style: titleStyle),
|
||||||
|
Text(value, style: valueStyle),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showErrorDialog(String message) {
|
||||||
|
if (!mounted) return;
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('حدث خطأ'),
|
||||||
|
content: Text(message),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
child: const Text('موافق'),
|
||||||
|
onPressed: () => Navigator.of(ctx).pop())
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showSuccessDialog() {
|
||||||
|
if (!mounted) return;
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('تم إرسال طلبك بنجاح'),
|
||||||
|
content: Text(
|
||||||
|
"سيتم تحويل المال إلى المحفظة التي أوردتها (${widget.walletType})، إلى الرقم ${widget.payoutPhoneNumber}، خلال مدة قصيرة. يرجى الانتظار، ستصلك رسالة تأكيد من محفظتك حال وصولها. شكراً لك."),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
child: const Text('موافق'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(ctx).pop();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,12 +7,14 @@ import 'package:sefer_driver/constant/colors.dart';
|
|||||||
import 'package:sefer_driver/constant/style.dart';
|
import 'package:sefer_driver/constant/style.dart';
|
||||||
import 'package:sefer_driver/controller/home/payment/captain_wallet_controller.dart';
|
import 'package:sefer_driver/controller/home/payment/captain_wallet_controller.dart';
|
||||||
import 'package:sefer_driver/controller/payment/payment_controller.dart';
|
import 'package:sefer_driver/controller/payment/payment_controller.dart';
|
||||||
|
import 'package:sefer_driver/controller/payment/smsPaymnet/payment_services.dart';
|
||||||
import 'package:webview_flutter/webview_flutter.dart';
|
import 'package:webview_flutter/webview_flutter.dart';
|
||||||
|
|
||||||
import '../../../constant/box_name.dart';
|
import '../../../constant/box_name.dart';
|
||||||
import '../../../constant/links.dart';
|
import '../../../constant/links.dart';
|
||||||
import '../../../controller/functions/crud.dart';
|
import '../../../controller/functions/crud.dart';
|
||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
|
import '../../../print.dart';
|
||||||
import '../../widgets/elevated_btn.dart';
|
import '../../widgets/elevated_btn.dart';
|
||||||
import '../../widgets/my_textField.dart';
|
import '../../widgets/my_textField.dart';
|
||||||
import 'ecash.dart';
|
import 'ecash.dart';
|
||||||
@@ -40,21 +42,29 @@ class PointsCaptain extends StatelessWidget {
|
|||||||
title: 'Which method you will pay'.tr,
|
title: 'Which method you will pay'.tr,
|
||||||
titleStyle: AppStyle.title,
|
titleStyle: AppStyle.title,
|
||||||
content: Column(
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${'you can buy '.tr}$countPoint ${'L.S'.tr}${'by '.tr}${'$pricePoint'.tr}',
|
'${'you can buy '.tr}$countPoint ${'L.S'.tr}${'by '.tr}${'$pricePoint'.tr}',
|
||||||
style: AppStyle.title,
|
style: AppStyle.title,
|
||||||
),
|
),
|
||||||
MyElevatedButton(
|
|
||||||
title: 'Pay with Credit Card'.tr,
|
|
||||||
onPressed: () async {
|
|
||||||
Get.back();
|
|
||||||
payWithEcashDriver(context, pricePoint.toString());
|
|
||||||
|
|
||||||
// var d = jsonDecode(res);
|
|
||||||
}, //51524
|
|
||||||
),
|
|
||||||
// Add some spacing between buttons
|
// Add some spacing between buttons
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
Get.back();
|
||||||
|
payWithEcashDriver(context, pricePoint.toString());
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text('Pay with Debit Card'.tr),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Icon(Icons.credit_card_sharp,
|
||||||
|
color: AppColor.blueColor, size: 70),
|
||||||
|
],
|
||||||
|
)),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
Get.back();
|
Get.back();
|
||||||
@@ -84,44 +94,122 @@ class PointsCaptain extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
child: Image.asset(
|
child: Row(
|
||||||
'assets/images/mtn.png',
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
width: 70,
|
children: [
|
||||||
height: 70,
|
Text('Pay by MTN Wallet'.tr),
|
||||||
fit: BoxFit.fill,
|
const SizedBox(width: 10),
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/cashMTN.png',
|
||||||
|
width: 70,
|
||||||
|
height: 70,
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
Get.back();
|
||||||
|
Get.defaultDialog(
|
||||||
|
barrierDismissible: false,
|
||||||
|
title: 'Insert Wallet phone number'.tr,
|
||||||
|
content: Form(
|
||||||
|
key: paymentController.formKey,
|
||||||
|
child: MyTextForm(
|
||||||
|
controller:
|
||||||
|
paymentController.walletphoneController,
|
||||||
|
label: 'Insert Wallet phone number'.tr,
|
||||||
|
hint: '963991234567',
|
||||||
|
type: TextInputType.phone)),
|
||||||
|
confirm: MyElevatedButton(
|
||||||
|
title: 'OK'.tr,
|
||||||
|
onPressed: () async {
|
||||||
|
Get.back();
|
||||||
|
if (paymentController.formKey.currentState!
|
||||||
|
.validate()) {
|
||||||
|
box.write(
|
||||||
|
BoxName.phoneWallet,
|
||||||
|
paymentController
|
||||||
|
.walletphoneController.text);
|
||||||
|
await payWithSyriaTelWallet(
|
||||||
|
context, pricePoint.toString(), 'SYP');
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text('Pay by Syriatel Wallet'.tr),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/syriatel.jpeg',
|
||||||
|
width: 70,
|
||||||
|
height: 70,
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
Get.back();
|
||||||
|
Get.defaultDialog(
|
||||||
|
barrierDismissible: false,
|
||||||
|
title: 'Insert Wallet phone number'.tr,
|
||||||
|
content: Form(
|
||||||
|
key: paymentController.formKey,
|
||||||
|
child: MyTextForm(
|
||||||
|
controller:
|
||||||
|
paymentController.walletphoneController,
|
||||||
|
label: 'Insert Wallet phone number'.tr,
|
||||||
|
hint: '963941234567',
|
||||||
|
type: TextInputType.phone)),
|
||||||
|
confirm: MyElevatedButton(
|
||||||
|
title: 'OK'.tr,
|
||||||
|
onPressed: () async {
|
||||||
|
Get.back();
|
||||||
|
if (paymentController.formKey.currentState!
|
||||||
|
.validate()) {
|
||||||
|
box.write(
|
||||||
|
BoxName.phoneWallet,
|
||||||
|
paymentController
|
||||||
|
.walletphoneController.text);
|
||||||
|
// await payWithSyriaTelWallet(
|
||||||
|
// context, pricePoint.toString(), 'SYP');
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Get.to(() => PaymentScreenSmsProvider(
|
||||||
|
amount: pricePoint));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text('Pay by Sham Cash'.tr),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/shamCash.png',
|
||||||
|
width: 70,
|
||||||
|
height: 70,
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
],
|
||||||
)),
|
)),
|
||||||
// MyElevatedButton(
|
|
||||||
// kolor: AppColor.redColor,
|
|
||||||
// title: 'Pay with Wallet'.tr,
|
|
||||||
// onPressed: () async {
|
|
||||||
// Get.back();
|
|
||||||
// Get.defaultDialog(
|
|
||||||
// barrierDismissible: false,
|
|
||||||
// title: 'Insert Wallet phone number'.tr,
|
|
||||||
// content: Form(
|
|
||||||
// key: paymentController.formKey,
|
|
||||||
// child: MyTextForm(
|
|
||||||
// controller:
|
|
||||||
// paymentController.walletphoneController,
|
|
||||||
// label: 'Insert Wallet phone number'.tr,
|
|
||||||
// hint: '963941234567',
|
|
||||||
// type: TextInputType.phone)),
|
|
||||||
// confirm: MyElevatedButton(
|
|
||||||
// title: 'OK'.tr,
|
|
||||||
// onPressed: () async {
|
|
||||||
// Get.back();
|
|
||||||
// if (paymentController.formKey.currentState!
|
|
||||||
// .validate()) {
|
|
||||||
// box.write(
|
|
||||||
// BoxName.phoneWallet,
|
|
||||||
// paymentController
|
|
||||||
// .walletphoneController.text);
|
|
||||||
// await payWithMTNWallet(
|
|
||||||
// context, pricePoint.toString(), 'SYP');
|
|
||||||
// }
|
|
||||||
// }));
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
],
|
],
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
@@ -485,7 +573,7 @@ Future<void> payWithMTNWallet(
|
|||||||
barrierDismissible: false);
|
barrierDismissible: false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String phone = box.read(BoxName.phoneWallet) ?? '963992952235';
|
String phone = box.read(BoxName.phoneWallet);
|
||||||
String driverID = box.read(BoxName.driverID).toString();
|
String driverID = box.read(BoxName.driverID).toString();
|
||||||
String formattedAmount = double.parse(amount).toStringAsFixed(0);
|
String formattedAmount = double.parse(amount).toStringAsFixed(0);
|
||||||
|
|
||||||
@@ -507,12 +595,13 @@ Future<void> payWithMTNWallet(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 1️⃣ استدعاء mtn_start_payment.php (الملف الجديد)
|
// 1️⃣ استدعاء mtn_start_payment.php (الملف الجديد)
|
||||||
var responseData = await CRUD().postWallet(
|
var responseData = await CRUD().postWalletMtn(
|
||||||
link: AppLink.payWithMTNStart,
|
link: AppLink.payWithMTNStart,
|
||||||
payload: {
|
payload: {
|
||||||
"amount": formattedAmount,
|
"amount": formattedAmount,
|
||||||
"passengerId": driverID,
|
"passengerId": driverID,
|
||||||
"phone": phone,
|
"phone": phone,
|
||||||
|
"lang": box.read(BoxName.lang) ?? 'ar',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -540,7 +629,7 @@ Future<void> payWithMTNWallet(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (startRes['status'] != 'success') {
|
if (startRes['status'] != 'success') {
|
||||||
String errorMsg = startRes['message']?.toString() ??
|
final errorMsg = startRes['message']['Error']?.toString().tr ??
|
||||||
"فشل بدء عملية الدفع. حاول مرة أخرى.";
|
"فشل بدء عملية الدفع. حاول مرة أخرى.";
|
||||||
throw Exception(errorMsg);
|
throw Exception(errorMsg);
|
||||||
}
|
}
|
||||||
@@ -555,7 +644,7 @@ Future<void> payWithMTNWallet(
|
|||||||
print(
|
print(
|
||||||
"📄 invoiceNumber: $invoiceNumber, 🔢 operationNumber: $operationNumber, 🧭 guid: $guid");
|
"📄 invoiceNumber: $invoiceNumber, 🔢 operationNumber: $operationNumber, 🧭 guid: $guid");
|
||||||
|
|
||||||
if (Get.isDialogOpen ?? false)
|
if (Get.isDialogOpen == true)
|
||||||
Get.back(); // إغلاق مؤشر التحميل قبل عرض حوار OTP
|
Get.back(); // إغلاق مؤشر التحميل قبل عرض حوار OTP
|
||||||
|
|
||||||
// 2️⃣ عرض واجهة إدخال OTP
|
// 2️⃣ عرض واجهة إدخال OTP
|
||||||
@@ -609,7 +698,8 @@ Future<void> payWithMTNWallet(
|
|||||||
if (Get.isDialogOpen ?? false) Get.back();
|
if (Get.isDialogOpen ?? false) Get.back();
|
||||||
|
|
||||||
print("✅ استجابة mtn_confirm.php:");
|
print("✅ استجابة mtn_confirm.php:");
|
||||||
print(confirmRes);
|
// print(confirmRes);
|
||||||
|
Log.print('confirmRes: ${confirmRes}');
|
||||||
|
|
||||||
if (confirmRes != null && confirmRes['status'] == 'success') {
|
if (confirmRes != null && confirmRes['status'] == 'success') {
|
||||||
Get.defaultDialog(
|
Get.defaultDialog(
|
||||||
@@ -635,3 +725,158 @@ Future<void> payWithMTNWallet(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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):");
|
||||||
|
Log.print('responseData: ${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().postWalletMtn(
|
||||||
|
// 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: ", "")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user