Update: 2026-06-11 21:53:27

This commit is contained in:
Hamza-Ayed
2026-06-11 21:53:27 +03:00
parent b87477bec4
commit 7049c7468c
8 changed files with 970 additions and 104 deletions

View File

@@ -0,0 +1,128 @@
<?php
// backend/ride/driverWallet/transfer.php
require_once __DIR__ . '/../../connect.php';
header('Content-Type: application/json');
// 1. Authenticate user
$decodedToken = authenticateJWT();
if (!$decodedToken) {
echo json_encode(['status' => '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
]);
}
?>

View File

@@ -88,6 +88,15 @@ static String get payWithSyriatelConfirm =>
"$paymentServer/ride/syriatel/driver/confirm_payment.php"; "$paymentServer/ride/syriatel/driver/confirm_payment.php";
static String get payWithSyriatelStart => static String get payWithSyriatelStart =>
"$paymentServer/ride/syriatel/driver/start_payment.php"; "$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 => static String get payWithEcashDriver =>
"$paymentServer/ride/ecash/driver/payWithEcash.php"; "$paymentServer/ride/ecash/driver/payWithEcash.php";
static String get payWithEcashPassenger => 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 updatePassengersWallet => "$wallet/update.php";
static String get getWalletByDriver => "$walletDriver/getWalletByDriver.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 => static String get driverStatistic =>
"$endPoint/ride/driverWallet/driverStatistic.php"; "$endPoint/ride/driverWallet/driverStatistic.php";
static String get getDriverDetails => static String get getDriverDetails =>

View File

@@ -1,8 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:local_auth/local_auth.dart';
import 'package:siro_driver/constant/style.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/controller/firebase/local_notification.dart';
import 'package:siro_driver/views/widgets/elevated_btn.dart'; import 'package:siro_driver/views/widgets/elevated_btn.dart';
import 'package:siro_driver/views/widgets/error_snakbar.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/constant/links.dart';
import 'package:siro_driver/controller/functions/crud.dart'; import 'package:siro_driver/controller/functions/crud.dart';
import 'package:siro_driver/main.dart'; import 'package:siro_driver/main.dart';
import 'package:siro_driver/views/widgets/mycircular.dart';
import '../../../views/widgets/mydialoug.dart'; import '../../../views/home/my_wallet/payment_screen_mtn.dart';
import '../../firebase/notification_service.dart'; import '../../../views/home/my_wallet/payment_screen_cliq.dart';
class CaptainWalletController extends GetxController { class CaptainWalletController extends GetxController {
bool isLoading = false; bool isLoading = false;
@@ -34,27 +33,42 @@ class CaptainWalletController extends GetxController {
final cardBank = TextEditingController(); final cardBank = TextEditingController();
final bankCode = 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 { payFromBudget() async {
if (formKey.currentState!.validate()) { if (formKey.currentState!.validate()) {
var pointFromBudget = int.parse((amountFromBudgetController.text)); var pointFromBudget = int.parse((amountFromBudgetController.text));
Get.dialog(const Center(child: MyCircularProgressIndicator()),
// await getPaymentId('fromBudgetToPoints', barrierDismissible: false);
// int.parse((amountFromBudgetController.text)) * -1); var res = await CRUD()
var paymentToken3 = .postWallet(link: AppLink.convertBudgetToPoints, payload: {
await generateToken((pointFromBudget * -1).toString()); 'amount': pointFromBudget.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,
'driverID': box.read(BoxName.driverID).toString(), 'driverID': box.read(BoxName.driverID).toString(),
}); });
Future.delayed(const Duration(seconds: 1)); Get.back(); // close loading
await addDriverWallet(
'fromBudget', pointFromBudget.toString(), pointFromBudget.toString()); if (res != 'failure') {
late Map<String, dynamic> mapRes;
if (res is String) {
mapRes = json.decode(res);
} else {
mapRes = res;
}
if (mapRes['status'] == 'success') {
update(); update();
Get.back(); Get.back();
await refreshCaptainWallet(); await refreshCaptainWallet();
@@ -64,6 +78,12 @@ class CaptainWalletController extends GetxController {
'tone1', 'tone1',
'', '',
); );
} else {
mySnackeBarError(mapRes['message']?.toString() ?? 'Error');
}
} else {
mySnackeBarError('Error processing request'.tr);
}
} }
} }
@@ -262,55 +282,26 @@ class CaptainWalletController extends GetxController {
} }
Future addTransferDriversWallet(String paymentMethod1, paymentMethod2) async { Future addTransferDriversWallet(String paymentMethod1, paymentMethod2) async {
var paymentID = Get.dialog(const Center(child: CircularProgressIndicator()),
await getPaymentId(paymentMethod1, amountFromBudgetController.text); barrierDismissible: false);
paymentToken = await generateToken( var res =
(int.parse(amountFromBudgetController.text) * -1).toString()); await CRUD().postWallet(link: AppLink.transferWalletDriver, payload: {
'amount': amountFromBudgetController.text,
await CRUD().postWallet(link: AppLink.addDrivePayment, payload: { 'receiverPhone': amountToNewDriverMap[0]['phone'].toString(),
'amount': (int.parse(amountFromBudgetController.text) * -1).toString(), 'country': box.read(BoxName.countryCode) ?? 'Egypt',
'rideId': paymentID.toString(),
'payment_method': paymentMethod1,
'passengerID': 'To ${amountToNewDriverMap[0]['id']}',
'token': paymentToken,
'driverID': box.read(BoxName.driverID).toString(), 'driverID': box.read(BoxName.driverID).toString(),
}); });
Get.back();
paymentID = await getPaymentId(paymentMethod2, if (res != 'failure') {
(int.parse(amountFromBudgetController.text) - 5).toString()); late Map<String, dynamic> mapRes;
paymentToken = await generateToken(amountFromBudgetController.text); if (res is String) {
var res1 = mapRes = json.decode(res);
await CRUD().postWallet(link: AppLink.addDriversWalletPoints, payload: { } else {
'driverID': amountToNewDriverMap[0]['id'].toString(), mapRes = res;
'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
'token': paymentToken,
'paymentMethod': paymentMethod2.toString(),
});
if (res1 != 'failure') {
// Get.find<FirebaseMessagesController>().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');
if (mapRes['status'] == 'success') {
Get.defaultDialog( Get.defaultDialog(
title: 'transfer Successful'.tr, title: 'transfer Successful'.tr,
middleText: '', middleText: '',
@@ -320,13 +311,16 @@ class CaptainWalletController extends GetxController {
onPressed: () async { onPressed: () async {
Get.back(); Get.back();
Get.back(); Get.back();
await refreshCaptainWallet(); await refreshCaptainWallet();
})); }));
} else {
mySnackeBarError(mapRes['message']?.toString() ?? 'Error');
}
} else {
mySnackeBarError('Error processing request'.tr);
} }
} }
getKazanPercent() async { getKazanPercent() async {
var res = await CRUD().get( var res = await CRUD().get(
link: AppLink.getKazanPercent, link: AppLink.getKazanPercent,
@@ -354,4 +348,110 @@ class CaptainWalletController extends GetxController {
await refreshCaptainWallet(); await refreshCaptainWallet();
super.onInit(); super.onInit();
} }
Future<void> 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<String, dynamic> resMap;
if (res is Map<String, dynamic>) {
resMap = res;
} else if (res is String) {
resMap = json.decode(res) as Map<String, dynamic>;
} 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<void> 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<String, dynamic> resMap;
if (res is Map<String, dynamic>) {
resMap = res;
} else if (res is String) {
resMap = json.decode(res) as Map<String, dynamic>;
} 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()));
}
}
} }

View File

@@ -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<PaymentScreenCliq> createState() => _PaymentScreenCliqState();
}
class _PaymentScreenCliqState extends State<PaymentScreenCliq> with SingleTickerProviderStateMixin {
Timer? _pollingTimer;
String _status = 'waiting'; // waiting, uploading, verifying, success, error
final TextEditingController _proofController = TextEditingController();
late AnimationController _blinkController;
late Animation<Color?> _colorAnimation;
late Animation<double> _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<double>(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<void> _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)),
),
),
],
);
}
}

View File

@@ -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<PaymentScreenMtn> createState() => _PaymentScreenMtnState();
}
class _PaymentScreenMtnState extends State<PaymentScreenMtn> with SingleTickerProviderStateMixin {
Timer? _pollingTimer;
String _status = 'waiting'; // waiting, uploading, verifying, success, error
final TextEditingController _proofController = TextEditingController();
late AnimationController _blinkController;
late Animation<Color?> _colorAnimation;
late Animation<double> _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<double>(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<void> _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)),
),
),
],
);
}
}

View File

@@ -6,6 +6,7 @@ import 'package:siro_driver/views/widgets/mycircular.dart';
import 'package:siro_driver/views/widgets/mydialoug.dart'; import 'package:siro_driver/views/widgets/mydialoug.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:siro_driver/constant/currency.dart';
import '../../../controller/home/payment/captain_wallet_controller.dart'; import '../../../controller/home/payment/captain_wallet_controller.dart';
@@ -93,7 +94,19 @@ class TransferBudgetPage extends StatelessWidget {
width: double.maxFinite, width: double.maxFinite,
decoration: AppStyle.boxDecoration1, decoration: AppStyle.boxDecoration1,
child: Text( 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, style: AppStyle.title,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@@ -106,27 +119,29 @@ class TransferBudgetPage extends StatelessWidget {
? MyElevatedButton( ? MyElevatedButton(
title: 'Transfer'.tr, title: 'Transfer'.tr,
onPressed: () async { onPressed: () async {
if (double.parse( double amount = double.tryParse(captainWalletController.amountFromBudgetController.text) ?? 0.0;
captainWalletController double totalAmount = double.tryParse(captainWalletController.totalAmountVisa) ?? 0.0;
.amountFromBudgetController double fee = captainWalletController.transferFee;
.text) < double minTransfer = captainWalletController.minTransferAmount;
double.parse(
captainWalletController if (amount < minTransfer) {
.totalAmountVisa) - MyDialog().getDialog(
5) { "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 await captainWalletController
.addTransferDriversWallet( .addTransferDriversWallet(
'TransferFrom', 'TransferFrom',
'TransferTo', '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() : const SizedBox()

View File

@@ -0,0 +1,67 @@
<?php
include '../../connect.php';
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
$driverID = filterRequest('driverID');
$amount = floatval(filterRequest('amount'));
if (empty($driverID) || empty($amount) || $amount <= 0) {
echo json_encode(['status' => '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()]);
}
?>

View File

@@ -0,0 +1,129 @@
<?php
include '../../jwtconnect.php';
// Disable error reporting output for production API
error_reporting(E_ALL);
ini_set('display_errors', 0);
// Set header
header('Content-Type: application/json');
$senderID = filterRequest('senderID');
$receiverID = filterRequest('receiverID'); // Now receiving the ID directly from Main Server
$amount = floatval(filterRequest('amount'));
$country = filterRequest('country'); // e.g. Egypt, Syria, Jordan
if (empty($senderID) || empty($receiverID) || empty($amount) || empty($country)) {
echo json_encode(['status' => '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()]);
}
?>