Initial push to my private server

This commit is contained in:
Hamza-Ayed
2025-09-21 15:02:12 +03:00
parent 7e904ae460
commit f08ee61a7e
32 changed files with 1622 additions and 373 deletions

View File

@@ -46,8 +46,8 @@ android {
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 29
targetSdk = 36
versionCode = 13
versionName = '1.0.13'
versionCode = 14
versionName = '1.0.14'
multiDexEnabled =true
}

BIN
assets/images/cashMTN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
assets/images/shamCash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/images/syriatel.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -11,6 +11,7 @@ class BoxName {
"rideArgumentsFromBackground";
static const String FCM_PRIVATE_KEY = "FCM_PRIVATE_KEY";
static const String hmac = "hmac";
static const String walletType = "walletType";
static const String fingerPrint = "fingerPrint";
static const String updateInterval = "updateInterval";
static const String payMobApikey = "payMobApikey";

View File

@@ -47,6 +47,8 @@ class AppLink {
static String getDriverToken = "$ride/firebase/getDriverToken.php";
static String addTokens = "$ride/firebase/add.php";
static String addTokensDriver = "$ride/firebase/addDriver.php";
static String addTokensDriverWallet =
"$seferPaymentServer/ride/firebase/addDriver.php";
//=======================Wallet===================
static String wallet = '$seferPaymentServer/ride/passengerWallet';
@@ -57,6 +59,10 @@ class AppLink {
"$seferPaymentServer/ride/mtn/driver/confirm_payment.php";
static String payWithMTNStart =
"$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 =
"$seferPaymentServer/ride/ecash/driver/payWithEcash.php";
static String payWithEcashPassenger =

View File

@@ -21,6 +21,7 @@ import 'package:location/location.dart';
import '../../../constant/api_key.dart';
import '../../../constant/info.dart';
import '../../../print.dart';
import '../../../views/auth/captin/otp_page.dart';
import '../../../views/auth/captin/otp_token_page.dart';
import '../../../views/auth/syria/pending_driver_page.dart';
@@ -98,11 +99,10 @@ class LoginDriverController extends GetxController {
}
isPhoneVerified() async {
var res = await CRUD().post(link: AppLink.isPhoneVerified, payload: {
'phone_number': box.read(
BoxName.phoneDriver,
)
});
var res = await CRUD().post(
link: AppLink.isPhoneVerified,
payload: {'phone_number': box.read(BoxName.phoneDriver)});
if (res != 'failure') {
// Get.offAll(() => SyrianCardAI());
Get.offAll(() => RegistrationView());
@@ -163,8 +163,8 @@ class LoginDriverController extends GetxController {
getJWT() async {
dev = Platform.isAndroid ? 'android' : 'ios';
// Log.print(
// 'box.read(BoxName.firstTimeLoadKey): ${box.read(BoxName.firstTimeLoadKey)}');
Log.print(
'box.read(BoxName.firstTimeLoadKey): ${box.read(BoxName.firstTimeLoadKey)}');
if (box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
var payload = {
'id': box.read(BoxName.driverID) ?? AK.newId,
@@ -185,13 +185,6 @@ class LoginDriverController extends GetxController {
final jwt = decodedResponse1['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();
@@ -206,7 +199,7 @@ class LoginDriverController extends GetxController {
'password': box.read(BoxName.emailDriver),
'aud': '${AK.allowed}$dev',
};
// print(payload);
print(payload);
var response1 = await http.post(
Uri.parse(AppLink.loginJwtDriver),
body: payload,
@@ -332,32 +325,30 @@ class LoginDriverController extends GetxController {
key: BoxName.fingerPrint, value: fingerPrint.toString());
// print(jsonDecode(token)['data'][0]['token'].toString());
// print(box.read(BoxName.tokenDriver).toString());
if (email == '962798583052@intaleqapp.com') {
} else {
if (token != 'failure') {
if ((jsonDecode(token)['data'][0]['token'].toString()) !=
box.read(BoxName.tokenDriver).toString()) {
await Get.defaultDialog(
barrierDismissible: false,
title: 'Device Change Detected'.tr,
middleText: 'Please verify your identity'.tr,
textConfirm: 'Verify'.tr,
confirmTextColor: Colors.white,
onConfirm: () {
// Get.back();
// انتقل لصفحة OTP الجديدة
Get.to(
() => OtpVerificationPage(
phone: d['phone'].toString(),
deviceToken: fingerPrint.toString(),
token: token.toString(),
ptoken:
jsonDecode(token)['data'][0]['token'].toString(),
),
);
},
);
}
if (token != 'failure') {
if ((jsonDecode(token)['data'][0]['token'].toString()) !=
box.read(BoxName.tokenDriver).toString()) {
await Get.defaultDialog(
barrierDismissible: false,
title: 'Device Change Detected'.tr,
middleText: 'Please verify your identity'.tr,
textConfirm: 'Verify'.tr,
confirmTextColor: Colors.white,
onConfirm: () {
// Get.back();
// انتقل لصفحة OTP الجديدة
Get.to(
() => OtpVerificationPage(
phone: d['phone'].toString(),
deviceToken: fingerPrint.toString(),
token: token.toString(),
ptoken:
jsonDecode(token)['data'][0]['token'].toString(),
),
);
},
);
}
}

View File

@@ -86,15 +86,28 @@ class OtpVerificationController extends GetxController {
},
);
if (response != 'failure' && response['status'] == 'success') {
if (response != 'failure') {
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',
'change device'.tr,
ptoken.toString(),
[],
'cancel.wav',
);
Get.offAll(() => HomeCaptain());
} else {
Get.snackbar('Verification Failed', 'OTP is incorrect or expired');

View File

@@ -94,22 +94,8 @@ class GoogleSignInHelper {
static Future<void> _handleSignOut() async {
// Clear stored driver information
box.remove(BoxName.driverID);
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);
box.erase();
storage.deleteAll();
Get.offAll(OnBoardingPage());
// Perform any additional sign-out tasks or API calls here
// For example, you can notify your server about the user sign-out

View File

@@ -1,18 +1,18 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:image_cropper/image_cropper.dart';
import 'package:image/image.dart' as img;
import 'package:path/path.dart';
import 'package:sefer_driver/constant/links.dart';
import '../../../constant/box_name.dart';
import 'package:path_provider/path_provider.dart';
// --- Final Submission ---
import 'dart:convert';
import 'dart:io';
import 'package:path_provider/path_provider.dart' as path_provider;
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart';
import 'package:mime/mime.dart';
@@ -21,8 +21,10 @@ import '../../../constant/colors.dart';
import '../../../constant/info.dart';
import '../../../main.dart';
import '../../../print.dart';
import '../../../views/widgets/error_snakbar.dart';
import '../../functions/crud.dart';
import '../../functions/encrypt_decrypt.dart';
import '../../functions/package_info.dart';
import '../captin/login_captin_controller.dart';
// You can create a simple enum to manage image types
@@ -41,6 +43,11 @@ class RegistrationController extends GetxController {
// Loading state
var isLoading = false.obs;
var isloading = false;
CroppedFile? croppedFile;
final picker = ImagePicker();
var image;
File? myImage;
String? colorHex; // سيُملى من الدروب داون
// Form Keys for validation
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)
Future<String> uploadToSyria({
required String docType,
@@ -317,30 +460,17 @@ class RegistrationController extends GetxController {
}
Future<void> submitRegistration() async {
// 1) تحقق من الصور
if (driverLicenseFrontImage == null ||
driverLicenseBackImage == null ||
carLicenseFrontImage == null ||
carLicenseBackImage == null) {
Get.snackbar(
'Missing Documents'.tr,
'Please upload all 4 required documents.'.tr,
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange,
colorText: Colors.white,
);
return;
}
// 0) دوال/مساعدات محلية
// 1) تحقق من وجود الروابط بدل الملفات
final driverFrontUrl = docUrls['driver_license_front'];
final driverBackUrl = docUrls['driver_license_back'];
final carFrontUrl = docUrls['car_license_front'];
final carBackUrl = docUrls['car_license_back'];
isLoading.value = true;
// روابط الـ API
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 registerUri = Uri.parse(AppLink.register_driver_and_car);
final client = http.Client();
try {
@@ -349,50 +479,7 @@ class RegistrationController extends GetxController {
'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}';
final hmac = '${box.read(BoxName.hmac)}';
// 2) ارفع الصور أولاً على السيرفر السوري واحصل على روابطها (Signed URLs)
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) جهّز طلب التسجيل الرئيسي: نرسل الحقول + روابط الصور (لا نرفع الصور مرة ثانية)
// 2) جهّز طلب التسجيل الرئيسي: حقول فقط + روابط الصور (لا نرفع صور إطلاقًا)
final req = http.MultipartRequest('POST', registerUri);
req.headers.addAll({
'Authorization': bearer,
@@ -411,18 +498,16 @@ class RegistrationController extends GetxController {
_addField(
fields, 'password', 'generate_your_password_here'); // عدّل حسب منطقك
_addField(fields, 'status', 'yet');
_addField(fields, 'email',
'Not specified'); // السيرفر سيحوّلها null ويبني ايميل افتراضي
_addField(fields, 'email', 'Not specified');
_addField(fields, 'gender', 'Male');
// --- Car Data ---
_addField(fields, 'vin', 'yet'); // تم تصحيح الاقتباس
_addField(fields, 'vin', 'yet');
_addField(fields, 'car_plate', carPlateController.text);
_addField(fields, 'make', carMakeController.text);
_addField(fields, 'model', carModelController.text);
_addField(fields, 'year', carYearController.text);
_addField(fields, 'expiration_date',
driverLicenseExpiryController.text); // تم التصحيح
_addField(fields, 'expiration_date', driverLicenseExpiryController.text);
_addField(fields, 'color', carColorController.text);
_addField(fields, 'fuel', 'Gasoline');
if (colorHex != null && colorHex!.isNotEmpty) {
@@ -431,32 +516,32 @@ class RegistrationController extends GetxController {
_addField(fields, 'owner',
'${firstNameController.text} ${lastNameController.text}');
// --- روابط الصور الموقّعة من سوريا ---
_addField(fields, 'driver_license_front', driverFrontUrl);
_addField(fields, 'driver_license_back', driverBackUrl);
_addField(fields, 'car_license_front', carFrontUrl);
_addField(fields, 'car_license_back', carBackUrl);
// --- روابط الصور المخزنة مسبقًا ---
_addField(fields, 'driver_license_front', driverFrontUrl!);
_addField(fields, 'driver_license_back', driverBackUrl!);
_addField(fields, 'car_license_front', carFrontUrl!);
_addField(fields, 'car_license_back', carBackUrl!);
// أضف الحقول
req.fields.addAll(fields);
// 4) الإرسال
// 3) الإرسال
final streamed =
await client.send(req).timeout(const Duration(seconds: 60));
final resp = await http.Response.fromStream(streamed);
// 5) فحص النتيجة
// 4) فحص النتيجة
Map<String, dynamic>? json;
try {
json = jsonDecode(resp.body) as Map<String, dynamic>;
} catch (_) {}
if (resp.statusCode == 200 && json?['status'] == 'success') {
final driverID =
(json!['data']?['driverID'] ?? json['driverID'])?.toString();
if (driverID != null && driverID.isNotEmpty) {
box.write(BoxName.driverID, driverID);
}
// final driverID =
// (json!['data']?['driverID'] ?? json['driverID'])?.toString();
// if (driverID != null && driverID.isNotEmpty) {
// box.write(BoxName.driverID, driverID);
// }
Get.snackbar(
'Success'.tr,
@@ -466,20 +551,31 @@ class RegistrationController extends GetxController {
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>()
? Get.find<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 {
final msg =
(json?['message'] ?? 'Registration failed. Please try again.')
.toString();
Log.print('msg: ${msg}');
Log.print('msg: $msg');
Get.snackbar(
'Error'.tr,
msg,
@@ -500,8 +596,7 @@ class RegistrationController extends GetxController {
client.close();
isLoading.value = false;
}
}
// Future<void> submitRegistration() async {
} // Future<void> submitRegistration() async {
// // 1) تحقق من الصور
// if (driverLicenseFrontImage == null ||
// driverLicenseBackImage == null ||

View File

@@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:io';
import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:sefer_driver/controller/functions/network/net_guard.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 '../../print.dart';
import 'gemeni.dart';
import 'network/connection_check.dart';
import 'upload_image.dart';
class CRUD {
@@ -86,24 +88,27 @@ class CRUD {
Map<String, dynamic>? payload,
required Map<String, String> headers,
}) 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 {
var response = await http.post(
url,
body: payload,
headers: headers,
// 1. Wrap the http.post call directly with HttpRetry.sendWithRetry.
// It will attempt the request immediately and retry on transient errors.
var response = await HttpRetry.sendWithRetry(
() {
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)
if (response.statusCode == 200) {
@@ -112,16 +117,18 @@ class CRUD {
if (jsonData['status'] == 'success') {
return jsonData; // Return the full JSON object on success
} else {
// The API reported a logical failure (e.g., validation error)
addError(
'API Logic Error: ${jsonData['status']}',
'Response: ${response.body}',
'CRUD._makeRequest - $link',
);
if (jsonData['status'] == 'failure') {
// return 'failure';
} else {
addError(
'API Logic Error: ${jsonData['status']}',
'Response: ${response.body}',
'CRUD._makeRequest - $link',
);
}
return jsonData['status']; // Return the specific status string
}
} catch (e, stackTrace) {
// Error decoding the JSON response from the server
addError(
'JSON Decode Error: $e',
'Response Body: ${response.body}\nStack Trace: $stackTrace',
@@ -130,20 +137,13 @@ class CRUD {
return 'failure';
}
}
// Handle Unauthorized (401) - typically means token expired
// Handle Unauthorized (401)
else if (response.statusCode == 401) {
var jsonData = jsonDecode(response.body);
if (jsonData['error'] == 'Token expired') {
// The token refresh logic is handled before the call,
// 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',
// );
await Get.put(LoginDriverController()).getJWT();
return 'token_expired';
} else {
// Other 401 errors (e.g., invalid token)
addError(
'Unauthorized Error: ${jsonData['error']}',
'Status Code: 401',
@@ -161,8 +161,14 @@ class CRUD {
);
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) {
// Handle network exceptions (e.g., no internet, DNS error)
// 3. This is a general catch-all for any other unexpected errors.
addError(
'HTTP Request Exception: $e',
'Stack Trace: $stackTrace',
@@ -177,15 +183,15 @@ class CRUD {
Map<String, dynamic>? payload,
}) async {
// 1. Check if the token is expired
bool isTokenExpired = JwtDecoder.isExpired(X
.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs)
.toString()
.split(AppInformation.addd)[0]);
// bool isTokenExpired = JwtDecoder.isExpired(X
// .r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs)
// .toString()
// .split(AppInformation.addd)[0]);
// 2. If expired, get a new one
if (isTokenExpired) {
await LoginDriverController().getJWT();
}
// // 2. If expired, get a new one
// if (isTokenExpired) {
// await LoginDriverController().getJWT();
// }
// 3. Prepare the headers with the valid token
final headers = {
@@ -303,15 +309,15 @@ class CRUD {
required String link,
Map<String, dynamic>? payload,
}) async {
bool isTokenExpired = JwtDecoder.isExpired(X
.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs)
.toString()
.split(AppInformation.addd)[0]);
// Log.print('isTokenExpired: ${isTokenExpired}');
// bool isTokenExpired = JwtDecoder.isExpired(X
// .r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs)
// .toString()
// .split(AppInformation.addd)[0]);
// // Log.print('isTokenExpired: ${isTokenExpired}');
if (isTokenExpired) {
await LoginDriverController().getJWT();
}
// if (isTokenExpired) {
// await LoginDriverController().getJWT();
// }
// await Get.put(LoginDriverController()).getJWT();
var url = Uri.parse(
link,

View File

@@ -109,8 +109,7 @@ class LocationController extends GetxController {
// ✅ تحديث للسيرفر
await CRUD().post(
link:
box.read(BoxName.serverChosen) + '/ride/location/update.php',
link: '${AppLink.server}/ride/location/update.php',
payload: payload,
);
@@ -141,7 +140,7 @@ class LocationController extends GetxController {
if (_insertCounter == 12) {
_insertCounter = 0;
await CRUD().post(
link: box.read(BoxName.serverChosen) + '/ride/location/add.php',
link: '${AppLink.server}/ride/location/add.php',
payload: payload,
);
}

View File

@@ -35,13 +35,13 @@ Future<void> showDriverGiftClaim(BuildContext context) async {
if (box.read(BoxName.is_claimed).toString() == '0' ||
box.read(BoxName.is_claimed) == null) {
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: {
'driverId': box.read(BoxName.driverID),
});
if (res != 'failure') {
Get.find<CaptainWalletController>()
.addDriverWallet('new driver', '300', '300');
.addDriverWallet('new driver', '30000', '30000');
Confetti.launch(
context,
options:

View File

@@ -77,7 +77,7 @@ class SmsEgyptController extends GetxController {
var res = await http.post(
Uri.parse(AppLink.checkCredit),
body: {
"username": AppInformation.appName,
"username": 'Sefer',
"password": AK.smsPasswordEgypt,
"message": "This is an example SMS message.",
"language": box.read(BoxName.lang) == 'en' ? "e" : 'r',

View File

@@ -426,9 +426,14 @@ class HomeCaptainController extends GetxController {
var res = await CRUD().get(
link: AppLink.getAllPaymentFromRide,
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();
}

View File

@@ -331,13 +331,7 @@ class MapDriverController extends GetxController {
'driverGoToPassengerTime': DateTime.now().toString(),
'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<FirebaseMessagesController>().sendNotificationToDriverMAP(

View 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 = "";
}
}

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:math';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
@@ -10,6 +11,7 @@ import 'package:sefer_driver/constant/colors.dart';
// استخدام نفس مسارات الاستيراد التي قدمتها
import '../../../constant/api_key.dart';
import '../../../constant/links.dart';
import '../../../print.dart';
import '../../functions/crud.dart';
import '../../functions/tts.dart';
@@ -525,45 +527,127 @@ class NavigationController extends GetxController {
String _parseInstruction(String html) =>
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 {
if (placeDestinationController.text.trim().isEmpty) {
final q = placeDestinationController.text.trim();
if (q.isEmpty) {
placesDestination = [];
update();
return;
}
if (myLocation == null) {
Get.snackbar('انتظر', 'جاري تحديد موقعك الحالي...');
return;
}
final query = placeDestinationController.text.trim();
final lat = myLocation!.latitude;
final lng = myLocation!.longitude;
const double range = 2.2;
final lat_min = lat - range,
lat_max = lat + range,
lng_min = lng - range,
lng_max = lng + range;
// نصف قطر البحث بالكيلومتر (عدّل حسب رغبتك)
const radiusKm = 200.0;
// حساب الباوند الصحيح (درجات، وليس 2.2 درجة ثابتة)
final latDelta = _kmToLatDelta(radiusKm);
final lngDelta = _kmToLngDelta(radiusKm, lat);
final latMin = lat - latDelta;
final latMax = lat + latDelta;
final lngMin = lng - lngDelta;
final lngMax = lng + lngDelta;
try {
final response = await CRUD().post(
link: AppLink.getPlacesSyria,
payload: {
'query': query,
'lat_min': lat_min.toString(),
'lat_max': lat_max.toString(),
'lng_min': lng_min.toString(),
'lng_max': lng_max.toString(),
'query': q,
'lat_min': latMin.toString(),
'lat_max': latMax.toString(),
'lng_min': lngMin.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 {
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);
});
// خذ أول 1015 للعرض (اختياري)، أو اعرض الكل
placesDestination = list.take(15).toList();
Log.print('placesDestination: $placesDestination');
update();
} catch (e) {
print('Exception in getPlaces: $e');
} finally {
update();
}
}

View File

@@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:local_auth/local_auth.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/views/widgets/error_snakbar.dart';
import 'package:get/get.dart';
@@ -27,48 +28,48 @@ class PaymobPayout extends GetxController {
sensitiveTransaction: true,
));
if (didAuthenticate) {
var dec = await CRUD()
.postWallet(link: AppLink.paymobPayoutDriverWallet, payload: {
"issuer": issuer,
"method": "wallet",
"amount": amount, //9.0,
"full_name":
'${box.read(BoxName.nameDriver)} ${box.read(BoxName.lastNameDriver)}',
"msisdn": msisdn, //"01010101010",
"bank_transaction_type": "cash_transfer"
});
if (dec['disbursement_status'] == 'successful') {
var paymentToken = await Get.find<CaptainWalletController>()
.generateToken(
((-1) * (double.parse(dec['amount'].toString())) - payOutFee)
.toStringAsFixed(0));
await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
'rideId': DateTime.now().toIso8601String(),
'amount':
((-1) * (double.parse(dec['amount'].toString())) - payOutFee)
.toStringAsFixed(0),
'payment_method': 'payout',
'passengerID': 'myself',
'token': paymentToken,
'driverID': box.read(BoxName.driverID).toString(),
});
await Get.find<CaptainWalletController>()
.addSeferWallet('payout fee myself', payOutFee.toString());
await updatePaymentToPaid(box.read(BoxName.driverID).toString());
await sendEmail(
box.read(BoxName.driverID).toString(),
amount,
box.read(BoxName.phoneDriver).toString(),
box.read(BoxName.nameDriver).toString(),
'Wallet',
box.read(BoxName.emailDriver).toString());
// var dec = await CRUD()
// .postWallet(link: AppLink.paymobPayoutDriverWallet, payload: {
// "issuer": issuer,
// "method": "wallet",
// "amount": amount, //9.0,
// "full_name":
// '${box.read(BoxName.nameDriver)} ${box.read(BoxName.lastNameDriver)}',
// "msisdn": msisdn, //"01010101010",
// "bank_transaction_type": "cash_transfer"
// });
// if (dec['disbursement_status'] == 'successful') {
// var paymentToken = await Get.find<CaptainWalletController>()
// .generateToken(
// ((-1) * (double.parse(dec['amount'].toString())) - payOutFee)
// .toStringAsFixed(0));
// await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
// 'rideId': DateTime.now().toIso8601String(),
// 'amount':
// ((-1) * (double.parse(dec['amount'].toString())) - payOutFee)
// .toStringAsFixed(0),
// 'payment_method': 'payout',
// 'passengerID': 'myself',
// 'token': paymentToken,
// 'driverID': box.read(BoxName.driverID).toString(),
// });
// await Get.find<CaptainWalletController>()
// .addSeferWallet('payout fee myself', payOutFee.toString());
// await updatePaymentToPaid(box.read(BoxName.driverID).toString());
// await sendEmail(
// box.read(BoxName.driverID).toString(),
// amount,
// box.read(BoxName.phoneDriver).toString(),
// box.read(BoxName.nameDriver).toString(),
// 'Wallet',
// box.read(BoxName.emailDriver).toString());
mySnackbarSuccess('${'Transaction successful'.tr} ${dec['amount']}');
// mySnackbarSuccess('${'Transaction successful'.tr} ${dec['amount']}');
Get.find<CaptainWalletController>().refreshCaptainWallet();
} else if (dec['disbursement_status'] == 'failed') {
mySnackeBarError('Transaction failed'.tr);
}
// Get.find<CaptainWalletController>().refreshCaptainWallet();
// } else if (dec['disbursement_status'] == 'failed') {
// mySnackeBarError('Transaction failed'.tr);
// }
} else {
MyDialog().getDialog('Authentication failed'.tr, ''.tr, () {
Get.back();

View File

@@ -63,7 +63,7 @@ class SplashScreenController extends GetxController
box.read(BoxName.onBoarding) == null
? Get.off(() => OnBoardingPage())
: box.read(BoxName.phoneDriver) != null &&
box.read(BoxName.phoneVerified) == '1'
box.read(BoxName.phoneVerified).toString() == '1'
? await Get.put(LoginDriverController())
.loginWithGoogleCredential(
box.read(BoxName.driverID).toString(),

View File

@@ -35,6 +35,8 @@ class MyTranslation extends Translations {
"نأسف لإعلامك بأن سائقًا آخر قد قبل هذا الطلب.",
"Driver Applied the Ride for You": "السائق قدم الطلب لك",
"Applied": "تم التقديم",
'Pay by Sham Cash': 'الدفع عبر شام كاش',
'Pay with Debit Card': 'الدفع ببطاقة الخصم',
"Please go to Car Driver": "يرجى الذهاب إلى سائق السيارة",
"Ok I will go now.": "حسنًا، سأذهب الآن.",
"Accepted Ride": "تم قبول الرحلة",
@@ -84,6 +86,8 @@ class MyTranslation extends Translations {
"Password must be at least 6 characters":
"يجب أن تتكون كلمة المرور من 6 أحرف على الأقل",
"Create Account": "إنشاء حساب",
'Pay by MTN Wallet': 'الدفع عبر محفظة MTN',
'Pay by Syriatel Wallet': 'الدفع عبر محفظة سيريتل',
"Login": "تسجيل الدخول",
"Back to other sign-in options": "العودة إلى خيارات التسجيل الأخرى",
"Driver Agreement": "اتفاقية السائق",
@@ -93,7 +97,11 @@ class MyTranslation extends Translations {
" and acknowledge our Privacy Policy.":
" والإقرار بسياسة الخصوصية الخاصة بنا.",
"I Agree": "أنا أوافق",
"Continue": "متابعة",
"Continue": "متابعة", "Customer not found": "العميل غير موجود",
"Wallet is blocked": "المحفظة محظورة",
"Customer phone is not active": "هاتف العميل غير نشط",
"Balance not enough": "الرصيد غير كافٍ",
"Balance limit exceeded": "تم تجاوز حد الرصيد",
"Privacy Policy": "سياسة الخصوصية",
"Location Access Required": "مطلوب الوصول إلى الموقع",
"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": "المعلومات الشخصية للسائق",
"First Name": "الاسم الأول",
"Last Name": "اسم العائلة",
'You have gift 30000 SYP': 'لديك هدية 30000 ليرة سورية',
"National ID Number": "الرقم الوطني",
"License Expiry Date": "تاريخ انتهاء الرخصة",
"YYYY-MM-DD": "YYYY-MM-DD",

View File

@@ -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 "حدث خطأ غير متوقع. يرجى المحاولة مرة أخرى.";
}
}
}

View 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),
);
}
}

View File

@@ -29,7 +29,7 @@ class CaptainProfileController extends GetxController {
await CRUD().post(link: AppLink.updateDriverEmail, payload: payload);
if ((res)['status'] == 'success') {
box.write(BoxName.email, emailController.text);
box.write(BoxName.emailDriver, emailController.text);
update();
Get.back();
} else {

View File

@@ -144,14 +144,28 @@ void main() async {
DeviceOrientation.portraitDown,
]);
runZonedGuarded<Future<void>>(() async {
// ... الكود الحالي الموجود في دالة main ...
runApp(const MyApp());
}, (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(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 ====
});
}

View File

@@ -284,6 +284,9 @@ class RegistrationView extends StatelessWidget {
// STEP 3
Widget _buildDocumentUploadStep(BuildContext ctx, RegistrationController c) {
final String linkUpload =
'https://syria.intaleq.xyz/intaleq/auth/syria/uploadImage.php';
return GetBuilder<RegistrationController>(
builder: (ctrl) => SingleChildScrollView(
padding: const EdgeInsets.all(16),
@@ -296,23 +299,31 @@ class RegistrationView extends StatelessWidget {
const SizedBox(height: 20),
_buildImagePickerBox(
'Driver License (Front)'.tr,
ctrl.driverLicenseFrontImage,
() => ctrl.pickImage(ImageType.driverLicenseFront),
ctrl.docUrls['driver_license_front'],
// () => ctrl.pickImage(ImageType.driverLicenseFront),
() async =>
await ctrl.choosImage(linkUpload, 'driver_license_front'),
),
_buildImagePickerBox(
'Driver License (Back)'.tr,
ctrl.driverLicenseBackImage,
() => ctrl.pickImage(ImageType.driverLicenseBack),
ctrl.docUrls['driver_license_back'],
() async =>
await ctrl.choosImage(linkUpload, 'driver_license_back'),
// () => ctrl.pickImage(ImageType.driverLicenseBack),
),
_buildImagePickerBox(
'Car Registration (Front)'.tr,
ctrl.carLicenseFrontImage,
() => ctrl.pickImage(ImageType.carLicenseFront),
ctrl.docUrls['car_license_front'],
() async =>
await ctrl.choosImage(linkUpload, 'car_license_front'),
// () => ctrl.pickImage(ImageType.carLicenseFront),
),
_buildImagePickerBox(
'Car Registration (Back)'.tr,
ctrl.carLicenseBackImage,
() => ctrl.pickImage(ImageType.carLicenseBack),
ctrl.docUrls['car_license_back'],
() 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(
margin: const EdgeInsets.only(bottom: 16),
child: InkWell(
@@ -337,8 +348,24 @@ class RegistrationView extends StatelessWidget {
child: SizedBox(
height: 150,
width: double.infinity,
child: img != null
? Image.file(img, fit: BoxFit.fill)
child: (img != null && img.isNotEmpty)
? 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(
mainAxisAlignment: MainAxisAlignment.center,
children: [

View File

@@ -1,5 +1,7 @@
import 'package:sefer_driver/constant/box_name.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/views/auth/captin/login_captin.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 '../../../../../controller/firebase/firbase_messge.dart';
import '../../../../../controller/functions/crud.dart';
import '../../../../../controller/functions/encrypt_decrypt.dart';
import '../../../../../controller/home/captin/order_request_controller.dart';
import '../../../../../controller/home/navigation/navigation_view.dart';
import '../../../../Rate/ride_calculate_driver.dart';
import '../../../../auth/syria/registration_view.dart';
import '../../../../widgets/error_snakbar.dart';
GetBuilder<HomeCaptainController> leftMainMenuCaptainIcons() {
final firebaseMessagesController =
@@ -181,10 +185,8 @@ GetBuilder<HomeCaptainController> leftMainMenuCaptainIcons() {
// child: Builder(builder: (context) {
// return IconButton(
// onPressed: () async {
// box.remove(BoxName.agreeTerms);
// Get.to(() => const NavigationView());
// // box.write(BoxName.statusDriverLocation, 'off');
// var finger = await storage.read(key: BoxName.fingerPrint);
//
// },
// icon: const Icon(
// FontAwesome5.grin_tears,

View File

@@ -6,6 +6,7 @@ import 'package:just_audio/just_audio.dart';
import 'package:sefer_driver/constant/api_key.dart';
import '../../../../constant/box_name.dart';
import '../../../../constant/links.dart';
import '../../../../controller/firebase/firbase_messge.dart';
import '../../../../controller/firebase/local_notification.dart';
import '../../../../controller/functions/crud.dart';
import '../../../../main.dart';
@@ -218,16 +219,22 @@ class _OrderOverlayState extends State<OrderOverlay>
'status': 'Apply',
'driver_id': box.read(BoxName.driverID),
});
if (AppLink.endPoint != AppLink.seferCairoServer) {
CRUD().post(
link: "${AppLink.endPoint}/ride/rides/updateStausFromSpeed.php",
payload: {
'id': orderData!.orderId,
'rideTimeStart': DateTime.now().toString(),
'status': 'Apply',
'driver_id': box.read(BoxName.driverID),
});
}
List<String> bodyToPassenger = [
_getData(6).toString(),
_getData(8).toString(),
_getData(9).toString(),
];
final fmc = Get.isRegistered<FirebaseMessagesController>()
? Get.find<FirebaseMessagesController>()
: Get.put(FirebaseMessagesController());
fmc.sendNotificationToDriverMAP(
"Accepted Ride",
'your ride is Accepted'.tr,
_getData(9).toString(),
bodyToPassenger,
'start.wav',
);
final payload = {
// بيانات أساسية
'driver_id': driverId,

View File

@@ -280,7 +280,7 @@ class _OrderRequestPageState extends State<OrderRequestPage> {
: Get.put(FirebaseMessagesController());
fmc.sendNotificationToDriverMAP(
"Accepted Ride".tr,
"Accepted Ride",
'your ride is Accepted'.tr,
controller.myList[9].toString(),
bodyToPassenger,

View File

@@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.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/colors.dart';
@@ -164,7 +165,7 @@ class CardSeferWalletDriver extends StatelessWidget {
void _showCashOutDialog(
BuildContext context, CaptainWalletController captainWalletController) {
double minAmount = 20.0; // الحد الأدنى للسحب
double minAmount = 20000.0; // الحد الأدنى للسحب
if (double.parse(captainWalletController.totalAmountVisa) >= minAmount) {
Get.defaultDialog(
barrierDismissible: false,
@@ -208,16 +209,29 @@ class CardSeferWalletDriver extends StatelessWidget {
confirm: MyElevatedButton(
title: 'تأكيد'.tr,
onPressed: () async {
box.write(
BoxName.phoneWallet, captainWalletController.phoneWallet);
box.write(BoxName.walletType,
Get.find<SyrianPayoutController>().dropdownValue.toString());
if (captainWalletController.formKey.currentState!.validate()) {
Get.back();
String amountAfterFee =
(double.parse(captainWalletController.totalAmountVisa) - 5)
.toStringAsFixed(0);
await Get.put(PaymobPayout()).payToWalletDriverAll(
amountAfterFee,
Get.find<SyrianPayoutController>().dropdownValue.toString(),
captainWalletController.phoneWallet.text.toString(),
);
Get.to(() => PayoutScreen(
amountToWithdraw:
double.parse(captainWalletController.totalAmountVisa),
payoutPhoneNumber:
captainWalletController.phoneWallet.text.toString(),
walletType: Get.find<SyrianPayoutController>()
.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,
@@ -274,7 +288,7 @@ class MyDropDownSyria extends StatelessWidget {
onChanged: (String? newValue) {
controller.changeValue(newValue);
},
items: <String>['syriatel', 'mtn']
items: <String>['Syriatel', 'Cash Mobile', 'Sham Cash']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
@@ -288,7 +302,7 @@ class MyDropDownSyria extends StatelessWidget {
// هذا المتحكم ضروري لعمل القائمة المنسدلة
class SyrianPayoutController extends GetxController {
String dropdownValue = 'syriatel';
String dropdownValue = 'Syriatel';
void changeValue(String? newValue) {
if (newValue != null) {

View 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();
},
),
],
),
);
}
}

View File

@@ -7,12 +7,14 @@ import 'package:sefer_driver/constant/colors.dart';
import 'package:sefer_driver/constant/style.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/smsPaymnet/payment_services.dart';
import 'package:webview_flutter/webview_flutter.dart';
import '../../../constant/box_name.dart';
import '../../../constant/links.dart';
import '../../../controller/functions/crud.dart';
import '../../../main.dart';
import '../../../print.dart';
import '../../widgets/elevated_btn.dart';
import '../../widgets/my_textField.dart';
import 'ecash.dart';
@@ -40,21 +42,29 @@ class PointsCaptain extends StatelessWidget {
title: 'Which method you will pay'.tr,
titleStyle: AppStyle.title,
content: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'${'you can buy '.tr}$countPoint ${'L.S'.tr}${'by '.tr}${'$pricePoint'.tr}',
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
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(
onTap: () async {
Get.back();
@@ -84,44 +94,122 @@ class PointsCaptain extends StatelessWidget {
}
}));
},
child: Image.asset(
'assets/images/mtn.png',
width: 70,
height: 70,
fit: BoxFit.fill,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Pay by MTN Wallet'.tr),
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);
try {
String phone = box.read(BoxName.phoneWallet) ?? '963992952235';
String phone = box.read(BoxName.phoneWallet);
String driverID = box.read(BoxName.driverID).toString();
String formattedAmount = double.parse(amount).toStringAsFixed(0);
@@ -507,12 +595,13 @@ Future<void> payWithMTNWallet(
}
// 1⃣ استدعاء mtn_start_payment.php (الملف الجديد)
var responseData = await CRUD().postWallet(
var responseData = await CRUD().postWalletMtn(
link: AppLink.payWithMTNStart,
payload: {
"amount": formattedAmount,
"passengerId": driverID,
"phone": phone,
"lang": box.read(BoxName.lang) ?? 'ar',
},
);
@@ -540,7 +629,7 @@ Future<void> payWithMTNWallet(
}
if (startRes['status'] != 'success') {
String errorMsg = startRes['message']?.toString() ??
final errorMsg = startRes['message']['Error']?.toString().tr ??
"فشل بدء عملية الدفع. حاول مرة أخرى.";
throw Exception(errorMsg);
}
@@ -555,7 +644,7 @@ Future<void> payWithMTNWallet(
print(
"📄 invoiceNumber: $invoiceNumber, 🔢 operationNumber: $operationNumber, 🧭 guid: $guid");
if (Get.isDialogOpen ?? false)
if (Get.isDialogOpen == true)
Get.back(); // إغلاق مؤشر التحميل قبل عرض حوار OTP
// 2⃣ عرض واجهة إدخال OTP
@@ -609,7 +698,8 @@ Future<void> payWithMTNWallet(
if (Get.isDialogOpen ?? false) Get.back();
print("✅ استجابة mtn_confirm.php:");
print(confirmRes);
// print(confirmRes);
Log.print('confirmRes: ${confirmRes}');
if (confirmRes != null && confirmRes['status'] == 'success') {
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: ", "")),
);
}
}