Files
Siro/siro_rider/lib/controller/payment/payment_controller.dart
2026-06-09 08:40:31 +03:00

769 lines
27 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:convert';
import 'package:siro_rider/constant/api_key.dart';
import 'package:siro_rider/constant/style.dart';
import 'package:siro_rider/controller/firebase/firbase_messge.dart';
import 'package:siro_rider/controller/payment/paymob/paymob_response.dart';
import 'package:siro_rider/views/home/map_page_passenger.dart';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:local_auth/local_auth.dart';
import 'package:siro_rider/controller/home/map/ride_lifecycle_controller.dart';
import 'package:siro_rider/controller/home/map/ride_state.dart';
import 'package:siro_rider/controller/home/map/ride_state.dart';
import 'package:webview_flutter/webview_flutter.dart';
import '../../constant/box_name.dart';
import '../../constant/colors.dart';
import '../../constant/info.dart';
import '../../constant/links.dart';
import '../../main.dart';
import '../../print.dart';
import '../firebase/notification_service.dart';
import '../functions/crud.dart';
import '../functions/encrypt_decrypt.dart';
import '../functions/toast.dart';
import 'paymob/e_cash_screen.dart';
class PaymentController extends GetxController {
bool isLoading = false;
bool isWalletChecked = true;
bool isCashChecked = false;
bool isWalletFound = false;
bool isPromoSheetDialogue = false;
final formKey = GlobalKey<FormState>();
final promo = TextEditingController();
final walletphoneController = TextEditingController();
double totalPassenger = Get.find<RideLifecycleController>().totalPassenger;
int? selectedAmount = 0;
List<dynamic> totalPassengerWalletDetails = [];
String passengerTotalWalletAmount = '';
String ip = '1';
DateTime now = DateTime.now();
late int timestamp;
void updateSelectedAmount(int value) {
selectedAmount = value;
update();
}
void changePromoSheetDialogue() {
isPromoSheetDialogue = !isPromoSheetDialogue;
update();
}
getPassengerWallet() async {
isLoading = true;
update();
await CRUD().getWallet(
link: AppLink.getWalletByPassenger,
payload: {'passenger_id': box.read(BoxName.passengerID)}).then((value) {
box.write(BoxName.passengerWalletTotal,
jsonDecode(value)['message'][0]['total'].toString());
});
isLoading = false;
update();
}
String paymentToken = '';
Future<String> generateTokenPassenger(String amount) async {
var res =
await CRUD().post(link: AppLink.addPaymentTokenPassenger, payload: {
'passengerId': box.read(BoxName.passengerID).toString(),
'amount': amount.toString(),
});
var d = jsonDecode(res);
return d['message'];
}
Future<String> generateTokenDriver(String amount) async {
var res = await CRUD().post(link: AppLink.addPaymentTokenDriver, payload: {
'driverID': Get.find<RideLifecycleController>().driverId,
'amount': amount.toString(),
});
var d = jsonDecode(res);
return d['message'];
}
Future addSeferWallet(String paymentMethod, point) async {
var seferToken = await generateTokenPassenger(point);
await CRUD().postWallet(link: AppLink.addSeferWallet, payload: {
'amount': point.toString(),
'paymentMethod': paymentMethod,
'passengerId': box.read(BoxName.passengerID).toString(),
'token': seferToken,
'driverId': 'passenger',
});
}
Future addPassengersWallet(String point) async {
var token = await generateTokenPassenger(point);
await CRUD().postWallet(link: AppLink.addPassengersWallet, payload: {
'passenger_id': box.read(BoxName.passengerID).toString(),
'balance': point,
'token': token,
});
}
payToDriverForCancelAfterAppliedAndHeNearYou(String rideId) async {
{
double costOfWaiting5Minute = box.read(BoxName.countryCode) == 'Egypt'
? (4 * .08) + (5 * 1)
// 4 indicate foe 4 km ditance from driver start move to passenger
: (4 * .06) + (5 * .06); //for Eygpt other like jordan .06 per minute
var paymentTokenWait =
await generateTokenDriver(costOfWaiting5Minute.toString());
var res =
await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
'rideId': rideId,
'amount': costOfWaiting5Minute.toString(),
'payment_method': 'cancel-from-near',
'passengerID': box.read(BoxName.passengerID).toString(),
'token': paymentTokenWait,
'driverID': Get.find<RideLifecycleController>().driverId.toString(),
});
var paymentTokenWait1 =
await generateTokenDriver(costOfWaiting5Minute.toString());
var res1 = await CRUD()
.postWallet(link: AppLink.addDriversWalletPoints, payload: {
'paymentID': 'rideId$rideId',
'amount': (costOfWaiting5Minute).toStringAsFixed(0),
'paymentMethod': 'cancel-from-near',
'token': paymentTokenWait1,
'driverID': Get.find<RideLifecycleController>().driverId.toString(),
});
if (res != 'failure') {
// Get.find<FirebaseMessagesController>().sendNotificationToDriverMAP(
// 'Cancel',
// 'Trip Cancelled. The cost of the trip will be added to your wallet.'
// .tr,
// Get.find<RideLifecycleController>().driverToken,
// [],
// 'cancel',
// );
await NotificationService.sendNotification(
category: 'Cancel',
target: Get.find<RideLifecycleController>().driverToken.toString(),
title: 'Cancel'.tr,
body:
'Trip Cancelled. The cost of the trip will be added to your wallet.'
.tr,
isTopic: false, // Important: this is a token
tone: 'cancel',
driverList: [],
);
}
var paymentTokenWaitPassenger1 =
await generateTokenPassenger((costOfWaiting5Minute * -1).toString());
await CRUD().post(link: AppLink.addPassengersWallet, payload: {
'passenger_id': box.read(BoxName.passengerID).toString(),
'balance': (costOfWaiting5Minute * -1).toString(),
'token': paymentTokenWaitPassenger1,
});
Get.offAll(const MapPagePassenger());
}
}
addPassengerWallet() async {
isLoading = true;
update();
await addSeferWallet('visa-in', selectedAmount.toString());
await addPassengersWallet(selectedAmount == 100
? '100'
: selectedAmount == 200
? '215'
: selectedAmount == 400
? '450'
: selectedAmount == 1000
? '1140'
: '0');
// getPassengerWallet();
isLoading = false;
update();
}
void onChangedPaymentMethodWallet(bool? value) {
if (box.read(BoxName.passengerWalletTotal) == null ||
double.parse(box.read(BoxName.passengerWalletTotal).toString()) <
totalPassenger) {
isWalletChecked = false;
isWalletChecked ? isCashChecked = true : isCashChecked = true;
update();
} else {
isWalletChecked = !isWalletChecked;
isWalletChecked ? isCashChecked = false : isCashChecked = true;
update();
}
}
void onChangedPaymentMethodCash(bool? value) {
if (box.read(BoxName.passengerWalletTotal) == null ||
double.parse(box.read(BoxName.passengerWalletTotal)) < totalPassenger) {
isWalletChecked = false;
isCashChecked = !isCashChecked;
isCashChecked ? isWalletChecked = false : isWalletChecked = false;
update();
} else {
isCashChecked = !isCashChecked;
isCashChecked ? isWalletChecked = false : isWalletChecked = true;
update();
}
}
void applyPromoCodeToPassenger() async {
//TAWJIHI
CRUD().get(link: AppLink.getPassengersPromo, payload: {
'promo_code': promo.text,
}).then((value) {
var decod = jsonDecode(value);
if (decod["status"] == "success") {
var firstElement = decod["message"][0];
totalPassenger = totalPassenger -
(totalPassenger * int.parse(firstElement['amount']));
Get.find<RideLifecycleController>().promoTaken = true;
update();
}
});
}
// 'https://accept.paymob.com/unifiedcheckout/?publicKey=egy_pk_live_mbjDC9Ni6FSHKmsz8sOHiVk2xd7oWRve&clientSecret=egy_sk_live_c0904e9cf04506ae64f818d4e075b4a957e3713fdf7a22cb7da30a29e72442b5'
// أضف هذا الرابط إلى ملف AppLink الخاص بك
// هذه هي الدالة الجديدة التي ستستخدمها لبدء الدفع
Future<void> payWithEcash(BuildContext context, String amount) async {
try {
// 1. يمكنك استخدام نفس طريقة التحقق بالبصمة إذا أردت
bool isAvailable = await LocalAuthentication().isDeviceSupported();
if (isAvailable) {
bool didAuthenticate = await LocalAuthentication().authenticate(
localizedReason: 'Use Touch ID or Face ID to confirm payment',
);
if (didAuthenticate) {
// 2. استدعاء الـ Endpoint الجديد على السيرفر الخاص بك
var res = await CRUD().postWallet(
link: AppLink.payWithEcashPassenger,
payload: {
// ✅ أرسل البيانات التي يحتاجها السيرفر الخاص بـ ecash
"amount": amount,
"passengerId": box.read(BoxName.passengerID),
},
);
// 3. التأكد من أن السيرفر أعاد رابط الدفع بنجاح
if (res != null &&
res['status'] == 'success' &&
res['message'] != null) {
final String paymentUrl = res['message'];
// 4. الانتقال إلى شاشة الدفع الجديدة الخاصة بـ ecash
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
EcashPaymentScreen(paymentUrl: paymentUrl),
),
);
} else {
// عرض رسالة خطأ في حال فشل السيرفر في إنشاء الرابط
Get.defaultDialog(
title: 'Error'.tr,
content: Text(
'Failed to initiate payment. Please try again.'.tr,
style: AppStyle.title,
),
);
}
}
}
} catch (e) {
Get.defaultDialog(
title: 'Error'.tr,
content: Text(
'An error occurred during the payment process.'.tr,
style: AppStyle.title,
),
);
}
}
// Future<void> payWithEcashDriver(BuildContext context, String amount) async {
// try {
// // يمكنك استخدام نفس طريقة التحقق بالبصمة إذا أردت
// bool isAvailable = await LocalAuthentication().isDeviceSupported();
// if (isAvailable) {
// bool didAuthenticate = await LocalAuthentication().authenticate(
// localizedReason: 'Use Touch ID or Face ID to confirm payment'.tr,
// );
// if (didAuthenticate) {
// // استدعاء الـ Endpoint الجديد على السيرفر الخاص بك
// var res = await CRUD().postWallet(
// link: AppLink.payWithEcashPassenger,
// // link:
// // 'https://wl.tripz-egypt.com/v1/main/ride/ecash/driver/payWithEcash.php',
// payload: {
// // أرسل البيانات التي يحتاجها السيرفر
// "amount": amount,
// // "driverId": box.read(BoxName.driverID), // تأكد من وجود هذا المتغير
// "passengerId":
// box.read(BoxName.passengerID), // تأكد من وجود هذا المتغير
// },
// );
// // التأكد من أن السيرفر أعاد رابط الدفع بنجاح
// if (res != null &&
// res['status'] == 'success' &&
// res['message'] != null) {
// final String paymentUrl = res['message'];
// // الانتقال إلى شاشة الدفع الجديدة الخاصة بـ ecash للسائق
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) =>
// EcashDriverPaymentScreen(paymentUrl: paymentUrl),
// ),
// );
// } else {
// // عرض رسالة خطأ في حال فشل السيرفر في إنشاء الرابط
// Get.defaultDialog(
// title: 'Error'.tr,
// content: Text(
// 'Failed to initiate payment. Please try again.'.tr,
// style: AppStyle.title,
// ),
// );
// }
// }
// }
// } catch (e) {
// Get.defaultDialog(
// title: 'Error'.tr,
// content: Text(
// 'An error occurred during the payment process.'.tr,
// style: AppStyle.title,
// ),
// );
// }
// }
/// شاشة جديدة ومبسطة خاصة بدفع السائقين عبر ecash
// Future<void> payWithMTNWallet(
// BuildContext context, String amount, String currency) async {
// // خزن سياق علوي آمن من البداية
// final BuildContext safeContext =
// Get.overlayContext ?? Get.context ?? context;
// // سبينر تحميل
// if (!(Get.isDialogOpen ?? false)) {
// Get.dialog(const Center(child: CircularProgressIndicator()),
// barrierDismissible: false);
// }
// try {
// final phone = box.read(BoxName.phoneWallet) as String;
// final passengerID = box.read(BoxName.passengerID).toString();
// final formattedAmount = double.parse(amount).toStringAsFixed(0);
// Log.print("🚀 بدء عملية دفع MTN");
// Log.print(
// "📦 Payload: passengerID: $passengerID, amount: $formattedAmount, phone: $phone");
// // التحقق بالبصمة (اختياري) + حماية من الـ await
// final localAuth = LocalAuthentication();
// final isAuthSupported = await localAuth.isDeviceSupported();
// if (isAuthSupported) {
// final didAuth = await localAuth.authenticate(
// localizedReason: 'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع',
// );
// if (!didAuth) {
// if (Get.isDialogOpen == true) Get.back();
// Log.print("❌ المستخدم لم يؤكد بالبصمة/الوجه");
// return;
// }
// }
// // 1) بدء الدفع
// final responseData = await CRUD().postWalletMtn(
// link: AppLink.payWithMTNStart,
// payload: {
// "amount": formattedAmount,
// "passengerId": passengerID,
// "phone": phone,
// "lang": box.read(BoxName.lang) ?? 'ar',
// },
// );
// // Log.print("✅ استجابة الخادم (mtn_start_payment.php):");
// // Log.print(responseData);
// Log.print('responseData: ${responseData}');
// // فحص الاستجابة بقوة
// late final Map<String, dynamic> startRes;
// if (responseData is Map<String, dynamic>) {
// startRes = responseData;
// } else if (responseData is String) {
// startRes = json.decode(responseData) as Map<String, dynamic>;
// } else {
// throw Exception("تم استلام نوع بيانات غير متوقع من الخادم.");
// }
// if (startRes['status'] != 'success') {
// final errorMsg = startRes['message']['Error']?.toString().tr ??
// "فشل بدء عملية الدفع. حاول مرة أخرى.";
// throw Exception(errorMsg);
// }
// final messageData = startRes["message"] as Map<String, dynamic>;
// final invoiceNumber = messageData["invoiceNumber"].toString();
// final operationNumber = messageData["operationNumber"].toString();
// final guid = messageData["guid"].toString();
// // Log.print(
// // "📄 invoiceNumber: $invoiceNumber, 🔢 operationNumber: $operationNumber, 🧭 guid: $guid");
// // أغلق السبينر قبل إظهار حوار OTP
// if (Get.isDialogOpen == true) Get.back();
// // 2) إدخال OTP بـ Get.defaultDialog (لا يستخدم context قابل للتلف)
// String otpInput = "";
// await Get.defaultDialog(
// title: "أدخل كود التحقق",
// barrierDismissible: false,
// content: TextField(
// keyboardType: TextInputType.number,
// decoration: const InputDecoration(hintText: "كود OTP"),
// onChanged: (v) => otpInput = v,
// ),
// confirm: TextButton(
// onPressed: () {
// if (otpInput.isEmpty ||
// otpInput.length < 4 ||
// otpInput.length > 8) {
// Get.snackbar("تنبيه", "أدخل كود OTP صحيح (48 أرقام)");
// return;
// }
// Get.back(result: otpInput);
// },
// child: const Text("تأكيد"),
// ),
// cancel: TextButton(
// onPressed: () => Get.back(result: null),
// child: const Text("إلغاء"),
// ),
// ).then((res) => otpInput = (res ?? "") as String);
// if (otpInput.isEmpty) {
// Log.print("❌ لم يتم إدخال OTP");
// return;
// }
// Log.print("🔐 تم إدخال OTP: $otpInput");
// // سبينر أثناء التأكيد
// Get.dialog(const Center(child: CircularProgressIndicator()),
// barrierDismissible: false);
// // 3) تأكيد الدفع
// final confirmRes = await CRUD().postWalletMtn(
// link: AppLink.payWithMTNConfirm,
// payload: {
// "invoiceNumber": invoiceNumber,
// "operationNumber": operationNumber,
// "guid": guid,
// "otp": otpInput,
// "phone": phone,
// "lang": box.read(BoxName.lang) ?? 'ar',
// },
// );
// if (Get.isDialogOpen == true) Get.back();
// // Log.print("✅ استجابة mtn_confirm.php:");
// // Log.print('confirmRes: ${confirmRes}');
// final ok = (confirmRes is Map && confirmRes['status'] == 'success');
// if (ok) {
// Get.defaultDialog(
// title: "✅ نجاح",
// content: const Text("تمت عملية الدفع وإضافة الرصيد إلى محفظتك."),
// );
// await getPassengerWallet();
// } else {
// final errorMsg = (confirmRes['message']['message']?.toString()) ??
// "فشل في تأكيد الدفع";
// Get.defaultDialog(title: "❌ فشل", content: Text(errorMsg.tr));
// }
// } catch (e, s) {
// Log.print("🔥 خطأ أثناء الدفع عبر MTN:");
// Log.print(e);
// Log.print(s);
// if (Get.isDialogOpen == true) Get.back();
// Get.defaultDialog(
// title: 'حدث خطأ',
// content: Text(e.toString().replaceFirst("Exception: ", "")),
// );
// }
// }
Future<void> payWithSyriaTelWallet(String amount, String currency) async {
// helper لفتح لودينغ بأمان
Future<void> _showLoading() async {
if (!(Get.isDialogOpen ?? false)) {
Get.dialog(const Center(child: CircularProgressIndicator()),
barrierDismissible: false);
}
}
// helper لإغلاق أي حوار مفتوح
void _closeAnyDialog() {
if (Get.isDialogOpen ?? false) {
Get.back();
}
}
await _showLoading();
try {
final phone = box.read(BoxName.phoneWallet) as String;
final passengerId = box.read(BoxName.passengerID).toString();
final formattedAmount = double.parse(amount).toStringAsFixed(0);
Log.print("🚀 Syriatel payment start");
Log.print(
"📦 Payload => passengerId:$passengerId amount:$formattedAmount phone:$phone");
// مصادقة حيوية (اختياري)
final auth = LocalAuthentication();
if (await auth.isDeviceSupported()) {
final ok = await auth.authenticate(
localizedReason: 'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع',
);
if (!ok) {
_closeAnyDialog();
Log.print("❌ User did not authenticate");
return;
}
}
// 1) بدء عملية الدفع
final startRaw = await CRUD().postWalletMtn(
link: AppLink.payWithSyriatelStart,
payload: {
"amount": formattedAmount,
"passengerId": passengerId,
"phone": phone,
"lang": box.read(BoxName.lang) ?? 'ar',
},
);
Log.print("✅ Server response (start): $startRaw");
// تحويل الاستجابة إلى Map
late final Map<String, dynamic> startRes;
if (startRaw is Map<String, dynamic>) {
startRes = startRaw;
} else if (startRaw is String) {
startRes = json.decode(startRaw) as Map<String, dynamic>;
} else {
throw Exception("Unexpected start response type");
}
if (startRes['status'] != 'success') {
final msg =
(startRes['message'] ?? 'Failed to start payment').toString();
throw Exception(msg);
}
final messageData = startRes['message'] as Map<String, dynamic>;
final transactionID = messageData['transactionID'].toString();
Log.print("📄 transactionID: $transactionID");
//
// 2) اطلب من المستخدم إدخال OTP عبر Get.dialog (بدون context)
_closeAnyDialog(); // أغلق اللودينغ أولاً
final otpController = TextEditingController();
final otp = await Get.dialog<String>(
AlertDialog(
title: const Text("أدخل كود التحقق"),
content: TextField(
controller: otpController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(hintText: "كود OTP"),
),
actions: [
TextButton(
child: const Text("تأكيد"),
onPressed: () => Get.back(result: otpController.text.trim()),
),
TextButton(
child: const Text("إلغاء"),
onPressed: () => Get.back(result: null),
),
],
),
barrierDismissible: false,
);
if (otp == null || otp.isEmpty) {
Log.print("❌ OTP not provided");
return;
}
Log.print("🔐 OTP: $otp");
await _showLoading();
// 3) تأكيد الدفع
final confirmRaw = await CRUD().postWallet(
link: AppLink.payWithSyriatelConfirm,
payload: {
"transactionID": transactionID,
"otp": otp,
},
);
_closeAnyDialog(); // أغلق اللودينغ
Log.print("✅ Response (confirm): $confirmRaw");
late final Map<String, dynamic> confirmRes;
if (confirmRaw is Map<String, dynamic>) {
confirmRes = confirmRaw;
} else if (confirmRaw is String) {
confirmRes = json.decode(confirmRaw) as Map<String, dynamic>;
} else {
throw Exception("Unexpected confirm response type");
}
if (confirmRes['status'] == 'success') {
Get.defaultDialog(
title: "✅ نجاح",
content: const Text("تمت عملية الدفع وإضافة الرصيد إلى محفظتك."),
);
} else {
final msg = (confirmRes['message'] ?? 'فشل في تأكيد الدفع').toString();
Get.defaultDialog(
title: "❌ فشل",
content: Text(msg),
);
}
} catch (e, s) {
Log.print("🔥 Error during Syriatel Wallet payment:\n$e\n$s");
_closeAnyDialog();
Get.defaultDialog(
title: 'حدث خطأ',
content: Text(e.toString().replaceFirst("Exception: ", "")),
);
}
}
@override
void onInit() {
timestamp = now.millisecondsSinceEpoch;
if (box.read(BoxName.passengerWalletTotal) == null) {
box.write(BoxName.passengerWalletTotal, '0');
}
getPassengerWallet();
final localAuth = LocalAuthentication();
super.onInit();
}
}
class EcashDriverPaymentScreen extends StatefulWidget {
final String paymentUrl;
const EcashDriverPaymentScreen({required this.paymentUrl, Key? key})
: super(key: key);
@override
State<EcashDriverPaymentScreen> createState() =>
_EcashDriverPaymentScreenState();
}
class _EcashDriverPaymentScreenState extends State<EcashDriverPaymentScreen> {
late final WebViewController _controller;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(NavigationDelegate(
onPageFinished: (url) async {
Log.print('Ecash Driver WebView URL Finished: $url');
await Get.find<PaymentController>().getPassengerWallet();
// هنا نتحقق فقط من أن المستخدم عاد إلى صفحة النجاح
// لا حاجة لاستدعاء أي API هنا، فالـ Webhook يقوم بكل العمل
if (url.contains("success.php")) {
showProcessingDialog();
}
},
))
..loadRequest(Uri.parse(widget.paymentUrl));
}
// دالة لعرض رسالة "العملية قيد المعالجة"
void showProcessingDialog() {
showDialog(
barrierDismissible: false,
context: Get.context!,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
title: Row(
children: [
Icon(Icons.check_circle, color: Colors.green),
const SizedBox(width: 8),
Text(
"Payment Successful".tr,
style: TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
],
),
content: Text(
"Your payment is being processed and your wallet will be updated shortly."
.tr,
style: const TextStyle(fontSize: 16),
),
actions: [
TextButton(
onPressed: () {
// أغلق مربع الحوار، ثم أغلق شاشة الدفع
Navigator.pop(context); // Close the dialog
Navigator.pop(context); // Close the payment screen
},
style: TextButton.styleFrom(
backgroundColor: Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
child: Text(
"OK".tr,
style: const TextStyle(color: Colors.white),
),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Complete Payment'.tr)),
body: WebViewWidget(controller: _controller),
);
}
}