diff --git a/backend/ride/driverWallet/transfer.php b/backend/ride/driverWallet/transfer.php new file mode 100644 index 0000000..5978751 --- /dev/null +++ b/backend/ride/driverWallet/transfer.php @@ -0,0 +1,128 @@ + 'error', 'message' => 'Unauthorized']); + exit; +} + +$senderID = filterRequest('driverID'); +$receiverPhone = filterRequest('receiverPhone'); +$amount = filterRequest('amount'); +$country = filterRequest('country'); + +if (empty($senderID) || empty($receiverPhone) || empty($amount) || empty($country)) { + echo json_encode(['status' => 'error', 'message' => 'Missing required fields']); + exit; +} + +// Ensure the sender matches the token +if ($decodedToken->id != $senderID) { + echo json_encode(['status' => 'error', 'message' => 'Unauthorized driver ID']); + exit; +} + +// 2. Fetch Receiver details +$stmt = $con->prepare("SELECT d.id as driver_id, dt.token as fcm_token, d.name_arabic + FROM driver d + LEFT JOIN driverToken dt ON d.id = dt.captain_id + WHERE d.phone = :phone LIMIT 1"); +$stmt->execute([':phone' => $receiverPhone]); +$receiver = $stmt->fetch(PDO::FETCH_ASSOC); + +if (!$receiver) { + echo json_encode(['status' => 'error', 'message' => 'Receiver not found']); + exit; +} + +$receiverID = $receiver['driver_id']; + +if ($receiverID == $senderID) { + echo json_encode(['status' => 'error', 'message' => 'Cannot transfer to yourself']); + exit; +} + +// 3. Determine Payment Server URL based on Country +$walletServer = "https://walletintaleq.intaleq.xyz"; // Default +if (strtolower($country) === 'jordan') { + $walletServer = getenv('WALLET_SERVER_JORDAN') ?: "https://walletintaleq.intaleq.xyz"; +} elseif (strtolower($country) === 'egypt') { + $walletServer = getenv('WALLET_SERVER_EGYPT') ?: "https://walletintaleq.intaleq.xyz"; +} elseif (strtolower($country) === 'syria') { + $walletServer = getenv('WALLET_SERVER_SYRIA') ?: "https://walletintaleq.intaleq.xyz"; +} + +$paymentServerUrl = "$walletServer/v2/main/ride/driverWallet/transfer.php"; + +$postData = [ + 'senderID' => $senderID, + 'receiverID' => $receiverID, + 'amount' => $amount, + 'country' => $country +]; + +// Generate Headers for Payment Server (Use internal payment key) +$headers = []; +$paymentKey = getenv('PAYMENT_KEY') ; + +if (!empty($paymentKey)) { + $headers[] = "payment-key: $paymentKey"; +} else { + // Fallback just in case + $headers[] = "payment-key: default_internal_secret_123"; +} + +$ch = curl_init(); +curl_setopt($ch, CURLOPT_URL, $paymentServerUrl); +curl_setopt($ch, CURLOPT_POST, 1); +curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData)); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + +$paymentResponseRaw = curl_exec($ch); +$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +curl_close($ch); + +$paymentResponse = json_decode($paymentResponseRaw, true); + +// 4. Handle Payment Server Response +if ($httpCode === 200 && isset($paymentResponse['status']) && $paymentResponse['status'] === 'success') { + // Transaction successful, send Push Notification + if (!empty($receiver['fcm_token'])) { + $senderName = $decodedToken->name ?? 'A driver'; // Optional: Fetch sender name + + $fcmBody = "You have received a transfer of " . $amount . " from " . $senderName; + // Arabic fallback if name available + $fcmBodyAr = "لقد تلقيت حوالة بقيمة " . $amount . " من " . $senderName; + + sendFCM_Internal( + $receiver['fcm_token'], + "Transfer Received", + $fcmBodyAr, + ['type' => 'transfer', 'amount' => $amount], + 'Transfer', + false, + 'ding' + ); + } + + echo json_encode([ + 'status' => 'success', + 'message' => 'Transfer completed successfully', + 'receiver' => $receiver['name_arabic'] + ]); +} else { + // Payment failed or server error + echo json_encode([ + 'status' => 'error', + 'message' => $paymentResponse['message'] ?? 'Payment server error', + 'debug' => $paymentResponseRaw + ]); +} +?> diff --git a/siro_driver/lib/constant/links.dart b/siro_driver/lib/constant/links.dart index 0d54c95..31d3123 100755 --- a/siro_driver/lib/constant/links.dart +++ b/siro_driver/lib/constant/links.dart @@ -88,6 +88,15 @@ static String get payWithSyriatelConfirm => "$paymentServer/ride/syriatel/driver/confirm_payment.php"; static String get payWithSyriatelStart => "$paymentServer/ride/syriatel/driver/start_payment.php"; + +static String get createMtnInvoice => "$paymentServer/ride/mtn_new/create_mtn_invoice.php"; +static String get uploadMtnProof => "$paymentServer/ride/mtn_new/verify_payment_ai.php"; +static String get checkMtnStatus => "$paymentServer/ride/mtn_new/check_status.php"; + +static String get createCliqInvoice => "$paymentServer/ride/cliq/create_cliq_invoice.php"; +static String get uploadCliqProof => "$paymentServer/ride/cliq/verify_payment_ai.php"; +static String get checkCliqStatus => "$paymentServer/ride/cliq/check_status.php"; + static String get payWithEcashDriver => "$paymentServer/ride/ecash/driver/payWithEcash.php"; static String get payWithEcashPassenger => @@ -102,6 +111,8 @@ static String get deletePassengersWallet => "$wallet/delete.php"; static String get updatePassengersWallet => "$wallet/update.php"; static String get getWalletByDriver => "$walletDriver/getWalletByDriver.php"; +static String get transferWalletDriver => "$endPoint/ride/driverWallet/transfer.php"; +static String get convertBudgetToPoints => "$walletDriver/convertBudgetToPoints.php"; static String get driverStatistic => "$endPoint/ride/driverWallet/driverStatistic.php"; static String get getDriverDetails => diff --git a/siro_driver/lib/controller/home/payment/captain_wallet_controller.dart b/siro_driver/lib/controller/home/payment/captain_wallet_controller.dart index 47e0224..ee9cf0e 100755 --- a/siro_driver/lib/controller/home/payment/captain_wallet_controller.dart +++ b/siro_driver/lib/controller/home/payment/captain_wallet_controller.dart @@ -1,8 +1,6 @@ import 'dart:convert'; -import 'package:local_auth/local_auth.dart'; import 'package:siro_driver/constant/style.dart'; -import 'package:siro_driver/controller/firebase/firbase_messge.dart'; import 'package:siro_driver/controller/firebase/local_notification.dart'; import 'package:siro_driver/views/widgets/elevated_btn.dart'; import 'package:siro_driver/views/widgets/error_snakbar.dart'; @@ -12,9 +10,10 @@ import 'package:siro_driver/constant/box_name.dart'; import 'package:siro_driver/constant/links.dart'; import 'package:siro_driver/controller/functions/crud.dart'; import 'package:siro_driver/main.dart'; +import 'package:siro_driver/views/widgets/mycircular.dart'; -import '../../../views/widgets/mydialoug.dart'; -import '../../firebase/notification_service.dart'; +import '../../../views/home/my_wallet/payment_screen_mtn.dart'; +import '../../../views/home/my_wallet/payment_screen_cliq.dart'; class CaptainWalletController extends GetxController { bool isLoading = false; @@ -34,36 +33,57 @@ class CaptainWalletController extends GetxController { final cardBank = TextEditingController(); final bankCode = TextEditingController(); + double get transferFee { + String country = box.read(BoxName.countryCode) ?? 'Jordan'; + if (country == 'Egypt') return 5.0; + if (country == 'Syria') return 10.0; + if (country == 'Jordan') return 0.25; + return 5.0; + } + + double get minTransferAmount { + String country = box.read(BoxName.countryCode) ?? 'Jordan'; + if (country == 'Egypt') return 10.0; + if (country == 'Syria') return 100.0; + if (country == 'Jordan') return 1.0; + return 10.0; + } payFromBudget() async { if (formKey.currentState!.validate()) { var pointFromBudget = int.parse((amountFromBudgetController.text)); - - // await getPaymentId('fromBudgetToPoints', - // int.parse((amountFromBudgetController.text)) * -1); - var paymentToken3 = - await generateToken((pointFromBudget * -1).toString()); - var paymentID = await getPaymentId( - 'fromBudgetToPoints', (pointFromBudget * -1).toString()); - await CRUD().postWallet(link: AppLink.addDrivePayment, payload: { - 'amount': (pointFromBudget * -1).toString(), - 'rideId': paymentID.toString(), - 'payment_method': 'myBudget', - 'passengerID': 'myBudgetToPoint', - 'token': paymentToken3, + Get.dialog(const Center(child: MyCircularProgressIndicator()), + barrierDismissible: false); + var res = await CRUD() + .postWallet(link: AppLink.convertBudgetToPoints, payload: { + 'amount': pointFromBudget.toString(), 'driverID': box.read(BoxName.driverID).toString(), }); - Future.delayed(const Duration(seconds: 1)); - await addDriverWallet( - 'fromBudget', pointFromBudget.toString(), pointFromBudget.toString()); - update(); - Get.back(); - await refreshCaptainWallet(); - NotificationController().showNotification( - 'You have successfully charged your account'.tr, - '$pointFromBudget ${'has been added to your budget'.tr}', - 'tone1', - '', - ); + Get.back(); // close loading + + if (res != 'failure') { + late Map mapRes; + if (res is String) { + mapRes = json.decode(res); + } else { + mapRes = res; + } + + if (mapRes['status'] == 'success') { + update(); + Get.back(); + await refreshCaptainWallet(); + NotificationController().showNotification( + 'You have successfully charged your account'.tr, + '$pointFromBudget ${'has been added to your budget'.tr}', + 'tone1', + '', + ); + } else { + mySnackeBarError(mapRes['message']?.toString() ?? 'Error'); + } + } else { + mySnackeBarError('Error processing request'.tr); + } } } @@ -262,71 +282,45 @@ class CaptainWalletController extends GetxController { } Future addTransferDriversWallet(String paymentMethod1, paymentMethod2) async { - var paymentID = - await getPaymentId(paymentMethod1, amountFromBudgetController.text); - paymentToken = await generateToken( - (int.parse(amountFromBudgetController.text) * -1).toString()); - - await CRUD().postWallet(link: AppLink.addDrivePayment, payload: { - 'amount': (int.parse(amountFromBudgetController.text) * -1).toString(), - 'rideId': paymentID.toString(), - 'payment_method': paymentMethod1, - 'passengerID': 'To ${amountToNewDriverMap[0]['id']}', - 'token': paymentToken, + Get.dialog(const Center(child: CircularProgressIndicator()), + barrierDismissible: false); + var res = + await CRUD().postWallet(link: AppLink.transferWalletDriver, payload: { + 'amount': amountFromBudgetController.text, + 'receiverPhone': amountToNewDriverMap[0]['phone'].toString(), + 'country': box.read(BoxName.countryCode) ?? 'Egypt', 'driverID': box.read(BoxName.driverID).toString(), }); + Get.back(); - paymentID = await getPaymentId(paymentMethod2, - (int.parse(amountFromBudgetController.text) - 5).toString()); - paymentToken = await generateToken(amountFromBudgetController.text); - var res1 = - await CRUD().postWallet(link: AppLink.addDriversWalletPoints, payload: { - 'driverID': amountToNewDriverMap[0]['id'].toString(), - 'paymentID': paymentID.toString(), - 'amount': ((int.parse(amountFromBudgetController.text) - 5)) - // kazan) // double.parse(kazan) .08 for egypt - .toStringAsFixed( - 0), // this will convert buddget to poitns by kazan .08 + if (res != 'failure') { + late Map mapRes; + if (res is String) { + mapRes = json.decode(res); + } else { + mapRes = res; + } - 'token': paymentToken, - 'paymentMethod': paymentMethod2.toString(), - }); - if (res1 != 'failure') { - // Get.find().sendNotificationToDriverMAP( - // 'Transfer', - // '${'You have transfer to your wallet from'.tr}' - // '${box.read(BoxName.nameDriver)}', - // amountToNewDriverMap[0]['token'].toString(), - // [], - // 'order1.wav'); - NotificationService.sendNotification( - target: amountToNewDriverMap[0]['token'].toString(), - title: 'Transfer'.tr, - body: '${'You have transfer to your wallet from'.tr}' - '${box.read(BoxName.nameDriver)}', - - isTopic: false, // Important: this is a token - tone: 'ding', - driverList: [], category: 'Transfer', - ); - await addSeferWallet('payout fee', '5'); - - Get.defaultDialog( - title: 'transfer Successful'.tr, - middleText: '', - titleStyle: AppStyle.title, - confirm: MyElevatedButton( - title: 'Ok'.tr, - onPressed: () async { - Get.back(); - Get.back(); - - await refreshCaptainWallet(); - })); + if (mapRes['status'] == 'success') { + Get.defaultDialog( + title: 'transfer Successful'.tr, + middleText: '', + titleStyle: AppStyle.title, + confirm: MyElevatedButton( + title: 'Ok'.tr, + onPressed: () async { + Get.back(); + Get.back(); + await refreshCaptainWallet(); + })); + } else { + mySnackeBarError(mapRes['message']?.toString() ?? 'Error'); + } + } else { + mySnackeBarError('Error processing request'.tr); } } - getKazanPercent() async { var res = await CRUD().get( link: AppLink.getKazanPercent, @@ -354,4 +348,110 @@ class CaptainWalletController extends GetxController { await refreshCaptainWallet(); super.onInit(); } + + Future payWithMTNWallet( + BuildContext context, String amount, String currency) async { + try { + final phone = phoneWallet.text.trim(); + if (phone.isEmpty) { + Get.defaultDialog( + title: 'Error'.tr, content: Text('Please enter phone number'.tr)); + return; + } + + Get.dialog(const Center(child: CircularProgressIndicator()), + barrierDismissible: false); + + var res = await CRUD().postWalletMtn( + link: AppLink.createMtnInvoice, + payload: { + "amount": amount, + "user_id": box.read(BoxName.driverID).toString(), + "user_type": "driver", + "mtn_phone": phone, + }, + ); + + Get.back(); // close loading + + late final Map resMap; + if (res is Map) { + resMap = res; + } else if (res is String) { + resMap = json.decode(res) as Map; + } else { + throw Exception("Unexpected response type"); + } + + if (resMap['status'] == 'success') { + Get.to(() => PaymentScreenMtn( + invoiceNumber: resMap['invoice_number'], + mtnNumber: resMap['mtn_payment_number'] ?? '---', + amount: double.parse(amount), + )); + } else { + Get.defaultDialog( + title: 'Error'.tr, + content: Text( + resMap['message']?.toString() ?? 'Failed to create invoice'.tr), + ); + } + } catch (e) { + if (Get.isDialogOpen ?? false) Get.back(); + Get.defaultDialog(title: 'Error'.tr, content: Text(e.toString())); + } + } + + Future payWithClickWallet( + BuildContext context, String amount, String currency) async { + try { + final phone = phoneWallet.text.trim(); + if (phone.isEmpty) { + Get.defaultDialog( + title: 'Error'.tr, content: Text('Please enter phone number'.tr)); + return; + } + + Get.dialog(const Center(child: CircularProgressIndicator()), + barrierDismissible: false); + + var res = await CRUD().postWalletMtn( + link: AppLink.createCliqInvoice, + payload: { + "amount": amount, + "user_id": box.read(BoxName.driverID).toString(), + "user_type": "driver", + "click_phone": phone, + }, + ); + + Get.back(); // close loading + + late final Map resMap; + if (res is Map) { + resMap = res; + } else if (res is String) { + resMap = json.decode(res) as Map; + } else { + throw Exception("Unexpected response type"); + } + + if (resMap['status'] == 'success') { + Get.to(() => PaymentScreenCliq( + invoiceNumber: resMap['invoice_number'], + cliqAlias: resMap['cliq_alias'] ?? '---', + amount: double.parse(amount), + )); + } else { + Get.defaultDialog( + title: 'Error'.tr, + content: Text( + resMap['message']?.toString() ?? 'Failed to create invoice'.tr), + ); + } + } catch (e) { + if (Get.isDialogOpen ?? false) Get.back(); + Get.defaultDialog(title: 'Error'.tr, content: Text(e.toString())); + } + } } diff --git a/siro_driver/lib/views/home/my_wallet/payment_screen_cliq.dart b/siro_driver/lib/views/home/my_wallet/payment_screen_cliq.dart new file mode 100644 index 0000000..97f1c83 --- /dev/null +++ b/siro_driver/lib/views/home/my_wallet/payment_screen_cliq.dart @@ -0,0 +1,208 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import '../../../constant/links.dart'; +import '../../../controller/functions/crud.dart'; + +class PaymentScreenCliq extends StatefulWidget { + final double amount; + final String invoiceNumber; + final String cliqAlias; + + const PaymentScreenCliq({ + Key? key, + required this.amount, + required this.invoiceNumber, + required this.cliqAlias, + }) : super(key: key); + + @override + State createState() => _PaymentScreenCliqState(); +} + +class _PaymentScreenCliqState extends State with SingleTickerProviderStateMixin { + Timer? _pollingTimer; + String _status = 'waiting'; // waiting, uploading, verifying, success, error + final TextEditingController _proofController = TextEditingController(); + + late AnimationController _blinkController; + late Animation _colorAnimation; + late Animation _shadowAnimation; + + @override + void initState() { + super.initState(); + _blinkController = AnimationController(duration: const Duration(milliseconds: 800), vsync: this)..repeat(reverse: true); + _colorAnimation = ColorTween(begin: Colors.red.shade700, end: Colors.red.shade100).animate(_blinkController); + _shadowAnimation = Tween(begin: 2.0, end: 15.0).animate(CurvedAnimation(parent: _blinkController, curve: Curves.easeInOut)); + + _startPolling(); + } + + @override + void dispose() { + _pollingTimer?.cancel(); + _blinkController.dispose(); + _proofController.dispose(); + super.dispose(); + } + + void _startPolling() { + _pollingTimer = Timer.periodic(const Duration(seconds: 5), (timer) async { + if (_status == 'success' || _status == 'verifying') return; + try { + final res = await CRUD().postWallet(link: AppLink.checkCliqStatus, payload: {'invoice_number': widget.invoiceNumber}); + if (res != 'failure' && res['status'] == 'success' && res['invoice_status'] == 'completed') { + timer.cancel(); + if (mounted) setState(() => _status = 'success'); + } + } catch (_) {} + }); + } + + Future _submitProof() async { + if (_proofController.text.trim().isEmpty) { + Get.snackbar('Error'.tr, 'Please paste the transfer message'.tr, backgroundColor: Colors.red); + return; + } + setState(() => _status = 'verifying'); + + try { + final res = await CRUD().postWallet(link: AppLink.uploadCliqProof, payload: { + 'invoice_number': widget.invoiceNumber, + 'proof_text': _proofController.text.trim(), + }); + + if (res != 'failure' && res['status'] == 'success') { + if (mounted) setState(() => _status = 'success'); + } else { + if (mounted) setState(() => _status = 'error'); + Get.defaultDialog(title: 'Failed'.tr, content: Text(res['message']?.toString() ?? 'Verification failed')); + } + } catch (e) { + if (mounted) setState(() => _status = 'error'); + Get.defaultDialog(title: 'Error'.tr, content: Text('Error uploading proof'.tr)); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + appBar: AppBar(title: const Text("Cliq Payment"), centerTitle: true, backgroundColor: Colors.white, foregroundColor: Colors.black), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: _status == 'success' ? _buildSuccessUI() : _buildWaitingUI(), + ), + ), + ); + } + + Widget _buildWaitingUI() { + final currencyFormat = NumberFormat.decimalPattern('ar_SY'); + + return SingleChildScrollView( + child: Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 15), + decoration: BoxDecoration(gradient: LinearGradient(colors: [Colors.blue.shade800, Colors.blue.shade600]), borderRadius: BorderRadius.circular(16)), + child: Column( + children: [ + const Text("المبلغ المطلوب", style: TextStyle(color: Colors.white70, fontSize: 14)), + const SizedBox(height: 5), + Text("${currencyFormat.format(widget.amount)} ل.س", style: const TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold)), + ], + ), + ), + const SizedBox(height: 25), + + AnimatedBuilder( + animation: _blinkController, + builder: (context, child) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: _colorAnimation.value ?? Colors.red, width: 3.0), boxShadow: [BoxShadow(color: (_colorAnimation.value ?? Colors.red).withOpacity(0.4), blurRadius: _shadowAnimation.value, spreadRadius: 2)]), + child: Column( + children: [ + const Text("يرجى تحويل المبلغ إلى الاسم المستعار التالي (Alias):", textAlign: TextAlign.center, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + const SizedBox(height: 15), + InkWell( + onTap: () { + Clipboard.setData(ClipboardData(text: widget.cliqAlias)); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: const Text("تم نسخ الاسم ✅", textAlign: TextAlign.center), backgroundColor: Colors.green.shade600)); + }, + child: Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration(color: Colors.blue.shade50, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.blue.shade200)), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(widget.cliqAlias, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2.0)), + const Icon(Icons.copy, color: Colors.blue), + ], + ), + ), + ), + ], + ), + ); + }, + ), + + const SizedBox(height: 30), + const Text("بعد إتمام التحويل، يرجى نسخ رسالة البنك النصية ولصقها هنا للتحقق التلقائي:", textAlign: TextAlign.center, style: TextStyle(fontSize: 14)), + const SizedBox(height: 10), + TextField( + controller: _proofController, + maxLines: 4, + decoration: InputDecoration( + hintText: "قم بلصق نص رسالة التحويل هنا...", + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), + filled: true, + fillColor: Colors.white, + ), + ), + const SizedBox(height: 15), + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: Colors.blue.shade800, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))), + onPressed: _status == 'verifying' ? null : _submitProof, + child: _status == 'verifying' + ? const CircularProgressIndicator(color: Colors.white) + : const Text("تحقق من الدفع", style: TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.bold)), + ), + ), + const SizedBox(height: 20), + const Text("جاري فحص الفاتورة تلقائياً كل 5 ثوانٍ...", style: TextStyle(color: Colors.grey, fontSize: 12)), + ], + ), + ); + } + + Widget _buildSuccessUI() { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.verified_rounded, color: Colors.green, size: 100), + const SizedBox(height: 20), + const Text("تم الدفع بنجاح!", style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), + const SizedBox(height: 40), + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: Colors.green, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16)), + onPressed: () { Get.back(); Get.back(); }, + child: const Text("متابعة", style: TextStyle(fontSize: 18)), + ), + ), + ], + ); + } +} diff --git a/siro_driver/lib/views/home/my_wallet/payment_screen_mtn.dart b/siro_driver/lib/views/home/my_wallet/payment_screen_mtn.dart new file mode 100644 index 0000000..4687602 --- /dev/null +++ b/siro_driver/lib/views/home/my_wallet/payment_screen_mtn.dart @@ -0,0 +1,208 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import '../../../constant/links.dart'; +import '../../../controller/functions/crud.dart'; + +class PaymentScreenMtn extends StatefulWidget { + final double amount; + final String invoiceNumber; + final String mtnNumber; + + const PaymentScreenMtn({ + Key? key, + required this.amount, + required this.invoiceNumber, + required this.mtnNumber, + }) : super(key: key); + + @override + State createState() => _PaymentScreenMtnState(); +} + +class _PaymentScreenMtnState extends State with SingleTickerProviderStateMixin { + Timer? _pollingTimer; + String _status = 'waiting'; // waiting, uploading, verifying, success, error + final TextEditingController _proofController = TextEditingController(); + + late AnimationController _blinkController; + late Animation _colorAnimation; + late Animation _shadowAnimation; + + @override + void initState() { + super.initState(); + _blinkController = AnimationController(duration: const Duration(milliseconds: 800), vsync: this)..repeat(reverse: true); + _colorAnimation = ColorTween(begin: Colors.red.shade700, end: Colors.red.shade100).animate(_blinkController); + _shadowAnimation = Tween(begin: 2.0, end: 15.0).animate(CurvedAnimation(parent: _blinkController, curve: Curves.easeInOut)); + + _startPolling(); + } + + @override + void dispose() { + _pollingTimer?.cancel(); + _blinkController.dispose(); + _proofController.dispose(); + super.dispose(); + } + + void _startPolling() { + _pollingTimer = Timer.periodic(const Duration(seconds: 5), (timer) async { + if (_status == 'success' || _status == 'verifying') return; + try { + final res = await CRUD().postWallet(link: AppLink.checkMtnStatus, payload: {'invoice_number': widget.invoiceNumber}); + if (res != 'failure' && res['status'] == 'success' && res['invoice_status'] == 'completed') { + timer.cancel(); + if (mounted) setState(() => _status = 'success'); + } + } catch (_) {} + }); + } + + Future _submitProof() async { + if (_proofController.text.trim().isEmpty) { + Get.snackbar('Error'.tr, 'Please paste the transfer message'.tr, backgroundColor: Colors.red); + return; + } + setState(() => _status = 'verifying'); + + try { + final res = await CRUD().postWallet(link: AppLink.uploadMtnProof, payload: { + 'invoice_number': widget.invoiceNumber, + 'proof_text': _proofController.text.trim(), + }); + + if (res != 'failure' && res['status'] == 'success') { + if (mounted) setState(() => _status = 'success'); + } else { + if (mounted) setState(() => _status = 'error'); + Get.defaultDialog(title: 'Failed'.tr, content: Text(res['message']?.toString() ?? 'Verification failed')); + } + } catch (e) { + if (mounted) setState(() => _status = 'error'); + Get.defaultDialog(title: 'Error'.tr, content: Text('Error uploading proof'.tr)); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + appBar: AppBar(title: const Text("MTN Payment"), centerTitle: true, backgroundColor: Colors.white, foregroundColor: Colors.black), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: _status == 'success' ? _buildSuccessUI() : _buildWaitingUI(), + ), + ), + ); + } + + Widget _buildWaitingUI() { + final currencyFormat = NumberFormat.decimalPattern('ar_SY'); + + return SingleChildScrollView( + child: Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 15), + decoration: BoxDecoration(gradient: LinearGradient(colors: [Colors.blue.shade800, Colors.blue.shade600]), borderRadius: BorderRadius.circular(16)), + child: Column( + children: [ + const Text("المبلغ المطلوب", style: TextStyle(color: Colors.white70, fontSize: 14)), + const SizedBox(height: 5), + Text("${currencyFormat.format(widget.amount)} ل.س", style: const TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold)), + ], + ), + ), + const SizedBox(height: 25), + + AnimatedBuilder( + animation: _blinkController, + builder: (context, child) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: _colorAnimation.value ?? Colors.red, width: 3.0), boxShadow: [BoxShadow(color: (_colorAnimation.value ?? Colors.red).withOpacity(0.4), blurRadius: _shadowAnimation.value, spreadRadius: 2)]), + child: Column( + children: [ + const Text("يرجى تحويل المبلغ إلى الرقم التالي:", textAlign: TextAlign.center, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + const SizedBox(height: 15), + InkWell( + onTap: () { + Clipboard.setData(ClipboardData(text: widget.mtnNumber)); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: const Text("تم نسخ الرقم ✅", textAlign: TextAlign.center), backgroundColor: Colors.green.shade600)); + }, + child: Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration(color: Colors.blue.shade50, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.blue.shade200)), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(widget.mtnNumber, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2.0)), + const Icon(Icons.copy, color: Colors.blue), + ], + ), + ), + ), + ], + ), + ); + }, + ), + + const SizedBox(height: 30), + const Text("بعد إتمام التحويل، يرجى نسخ رسالة البنك النصية ولصقها هنا للتحقق التلقائي:", textAlign: TextAlign.center, style: TextStyle(fontSize: 14)), + const SizedBox(height: 10), + TextField( + controller: _proofController, + maxLines: 4, + decoration: InputDecoration( + hintText: "قم بلصق نص رسالة التحويل هنا...", + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), + filled: true, + fillColor: Colors.white, + ), + ), + const SizedBox(height: 15), + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: Colors.blue.shade800, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))), + onPressed: _status == 'verifying' ? null : _submitProof, + child: _status == 'verifying' + ? const CircularProgressIndicator(color: Colors.white) + : const Text("تحقق من الدفع", style: TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.bold)), + ), + ), + const SizedBox(height: 20), + const Text("جاري فحص الفاتورة تلقائياً كل 5 ثوانٍ...", style: TextStyle(color: Colors.grey, fontSize: 12)), + ], + ), + ); + } + + Widget _buildSuccessUI() { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.verified_rounded, color: Colors.green, size: 100), + const SizedBox(height: 20), + const Text("تم الدفع بنجاح!", style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), + const SizedBox(height: 40), + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: Colors.green, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16)), + onPressed: () { Get.back(); Get.back(); }, + child: const Text("متابعة", style: TextStyle(fontSize: 18)), + ), + ), + ], + ); + } +} diff --git a/siro_driver/lib/views/home/my_wallet/transfer_budget_page.dart b/siro_driver/lib/views/home/my_wallet/transfer_budget_page.dart index 86f54c9..5d20eff 100755 --- a/siro_driver/lib/views/home/my_wallet/transfer_budget_page.dart +++ b/siro_driver/lib/views/home/my_wallet/transfer_budget_page.dart @@ -6,6 +6,7 @@ import 'package:siro_driver/views/widgets/mycircular.dart'; import 'package:siro_driver/views/widgets/mydialoug.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:siro_driver/constant/currency.dart'; import '../../../controller/home/payment/captain_wallet_controller.dart'; @@ -93,7 +94,19 @@ class TransferBudgetPage extends StatelessWidget { width: double.maxFinite, decoration: AppStyle.boxDecoration1, child: Text( - "${"amount".tr} ${captainWalletController.amountFromBudgetController.text} ${'LE'.tr}", + "${"amount".tr} ${captainWalletController.amountFromBudgetController.text} ${CurrencyHelper.currency}", + style: AppStyle.title, + textAlign: TextAlign.center, + ), + ), + const SizedBox( + height: 5, + ), + Container( + width: double.maxFinite, + decoration: AppStyle.boxDecoration1, + child: Text( + "${"Transfer Fee".tr}: ${captainWalletController.transferFee} ${CurrencyHelper.currency}", style: AppStyle.title, textAlign: TextAlign.center, ), @@ -106,27 +119,29 @@ class TransferBudgetPage extends StatelessWidget { ? MyElevatedButton( title: 'Transfer'.tr, onPressed: () async { - if (double.parse( - captainWalletController - .amountFromBudgetController - .text) < - double.parse( - captainWalletController - .totalAmountVisa) - - 5) { + double amount = double.tryParse(captainWalletController.amountFromBudgetController.text) ?? 0.0; + double totalAmount = double.tryParse(captainWalletController.totalAmountVisa) ?? 0.0; + double fee = captainWalletController.transferFee; + double minTransfer = captainWalletController.minTransferAmount; + + if (amount < minTransfer) { + MyDialog().getDialog( + "Error".tr, + "${"Minimum transfer amount is".tr} $minTransfer ${CurrencyHelper.currency}", () { + Get.back(); + }); + } else if (amount > (totalAmount - fee)) { + MyDialog().getDialog( + "Insufficient Balance".tr, + "${"You must leave at least".tr} $fee ${CurrencyHelper.currency} ${"for transfer fees".tr}", () { + Get.back(); + }); + } else { await captainWalletController .addTransferDriversWallet( 'TransferFrom', 'TransferTo', ); - } else { - MyDialog().getDialog( - "You dont have money in your Wallet" - .tr, - "You dont have money in your Wallet or you should less transfer 5 LE to activate" - .tr, () { - Get.back(); - }); } }) : const SizedBox() diff --git a/walletintaleq.intaleq.xyz/v2/main/ride/driverWallet/convertBudgetToPoints.php b/walletintaleq.intaleq.xyz/v2/main/ride/driverWallet/convertBudgetToPoints.php new file mode 100644 index 0000000..f4799c6 --- /dev/null +++ b/walletintaleq.intaleq.xyz/v2/main/ride/driverWallet/convertBudgetToPoints.php @@ -0,0 +1,67 @@ + 'error', 'message' => 'Missing required fields or invalid amount']); + exit; +} + +try { + $con->beginTransaction(); + + // 1. Fetch current budget + $stmt = $con->prepare("SELECT SUM(amount) as diff FROM payments WHERE captain_id = :driverID FOR UPDATE"); + $stmt->execute([':driverID' => $driverID]); + $sumRow = $stmt->fetch(PDO::FETCH_ASSOC); + $totalBudget = floatval($sumRow['diff']); + + if ($totalBudget < $amount) { + $con->rollBack(); + echo json_encode(['status' => 'error', 'message' => 'Insufficient budget']); + exit; + } + + // 2. Generate unique tokens + $paymentID1 = "budget2pt_" . time() . rand(1000, 9999); + $paymentID2 = "pt2budget_" . time() . rand(1000, 9999); + $token1 = md5(uniqid("b1", true)); + $token2 = md5(uniqid("b2", true)); + + // 3. Deduct from budget (payments) + $deductAmount = -$amount; + $stmt = $con->prepare("INSERT INTO payments (captain_id, amount, rideId, payment_method, passengerID, token) + VALUES (:driverID, :amount, :rideId, 'myBudget', 'myBudgetToPoint', :token)"); + $stmt->execute([ + ':driverID' => $driverID, + ':amount' => $deductAmount, + ':rideId' => $paymentID1, + ':token' => $token1 + ]); + + // 4. Add to points (paymentsDriverPoints) + $stmt = $con->prepare("INSERT INTO paymentsDriverPoints (captain_id, paymentID, amount, token, paymentMethod) + VALUES (:driverID, :paymentID, :amount, :token, 'fromBudget')"); + $stmt->execute([ + ':driverID' => $driverID, + ':paymentID' => $paymentID2, + ':amount' => $amount, + ':token' => $token2 + ]); + + // Commit Transaction + $con->commit(); + + echo json_encode(['status' => 'success', 'message' => 'Budget converted to points successfully']); + +} catch (Exception $e) { + $con->rollBack(); + echo json_encode(['status' => 'error', 'message' => 'Database transaction failed: ' . $e->getMessage()]); +} +?> diff --git a/walletintaleq.intaleq.xyz/v2/main/ride/driverWallet/transfer.php b/walletintaleq.intaleq.xyz/v2/main/ride/driverWallet/transfer.php new file mode 100644 index 0000000..c2901da --- /dev/null +++ b/walletintaleq.intaleq.xyz/v2/main/ride/driverWallet/transfer.php @@ -0,0 +1,129 @@ + 'error', 'message' => 'Missing required fields']); + exit; +} +// --- Payment Key Authentication --- +$expectedKey = getenv('PAYMENT_KEY'); +$providedKey = $_SERVER['HTTP_PAYMENT_KEY'] ?? ''; + +if (empty($expectedKey) || $providedKey !== $expectedKey) { + http_response_code(401); + echo json_encode(['status' => 'error', 'message' => 'Unauthorized Payment Server Access (Invalid Key)']); + exit; +} +// 1. Determine Fee based on Country +$fee = 0; +if (strtolower($country) === 'egypt') { + $fee = 5; + if ($amount < 10) { + echo json_encode(['status' => 'error', 'message' => 'Minimum transfer amount in Egypt is 10']); + exit; + } +} elseif (strtolower($country) === 'syria') { + $fee = 10; + if ($amount < 100) { + echo json_encode(['status' => 'error', 'message' => 'Minimum transfer amount in Syria is 100']); + exit; + } +} elseif (strtolower($country) === 'jordan') { + $fee = 0.25; + if ($amount < 1) { + echo json_encode(['status' => 'error', 'message' => 'Minimum transfer amount in Jordan is 1']); + exit; + } +} else { + // Default fee if unknown + $fee = 5; +} + +try { + $con->beginTransaction(); + + if ($receiverID == $senderID) { + $con->rollBack(); + echo json_encode(['status' => 'error', 'message' => 'Cannot transfer to yourself']); + exit; + } + + // 2. Fetch Sender Budget (with FOR UPDATE to lock rows) + $stmt = $con->prepare("SELECT SUM(amount) as diff FROM payments WHERE captain_id = :senderID FOR UPDATE"); + $stmt->execute([':senderID' => $senderID]); + $sumRow = $stmt->fetch(PDO::FETCH_ASSOC); + $totalBudget = floatval($sumRow['diff']); + + if ($totalBudget < $amount) { + $con->rollBack(); + echo json_encode(['status' => 'error', 'message' => 'Insufficient budget']); + exit; + } + + $amountForReceiver = $amount - $fee; + if ($amountForReceiver <= 0) { + $con->rollBack(); + echo json_encode(['status' => 'error', 'message' => 'Transfer amount must be greater than the fee']); + exit; + } + + // 3. Generate unique Tokens and paymentIDs + $paymentID1 = "transfer_" . time() . rand(1000, 9999); + $paymentID2 = "transfer_recv_" . time() . rand(1000, 9999); + $token1 = md5(uniqid("tk1", true)); + $token2 = md5(uniqid("tk2", true)); + $seferToken = md5(uniqid("sfr", true)); + + // 4. Deduct from Sender (payments table) + $deductAmount = -$amount; + $stmt = $con->prepare("INSERT INTO payments (captain_id, amount, rideId, payment_method, passengerID, token) + VALUES (:senderID, :amount, :rideId, 'cash_transfer', :receiverRef, :token)"); + $stmt->execute([ + ':senderID' => $senderID, + ':amount' => $deductAmount, + ':rideId' => $paymentID1, + ':receiverRef' => 'To ' . $receiverID, + ':token' => $token1 + ]); + + // 5. Add to Receiver Points (paymentsDriverPoints table) + $stmt = $con->prepare("INSERT INTO paymentsDriverPoints (captain_id, paymentID, amount, token, paymentMethod) + VALUES (:receiverID, :paymentID, :amount, :token, 'Transfer')"); + $stmt->execute([ + ':receiverID' => $receiverID, + ':paymentID' => $paymentID2, + ':amount' => $amountForReceiver, + ':token' => $token2 + ]); + + // 6. Add Fee to Sefer Wallet + $stmt = $con->prepare("INSERT INTO seferWallet (amount, paymentMethod, passengerId, token, driverId) + VALUES (:fee, 'payout fee', 'driver', :token, :senderID)"); + $stmt->execute([ + ':fee' => $fee, + ':token' => $seferToken, + ':senderID' => $senderID + ]); + + // Commit Transaction + $con->commit(); + + echo json_encode(['status' => 'success', 'message' => 'Transfer completed successfully on payment server']); + +} catch (Exception $e) { + $con->rollBack(); + echo json_encode(['status' => 'error', 'message' => 'Database transaction failed: ' . $e->getMessage()]); +} +?>