import 'dart:convert'; import 'package:Tripz/constant/box_name.dart'; import 'package:Tripz/constant/links.dart'; import 'package:Tripz/controller/functions/crud.dart'; import 'package:Tripz/main.dart'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:webview_flutter/webview_flutter.dart'; import 'package:http/http.dart' as http; import '../../../print.dart'; import '../../functions/encrypt_decrypt.dart'; class PaymobResponse { bool success; String? transactionID; String? responseCode; String? message; PaymobResponse({ this.transactionID, required this.success, this.responseCode, this.message, }); factory PaymobResponse.fromJson(Map json) { return PaymobResponse( success: json['success'] == 'true', transactionID: json['id'], message: json['message'], responseCode: json['txn_response_code'], ); } } class PaymobPayment { static PaymobPayment instance = PaymobPayment(); bool _isInitialized = false; final Dio _dio = Dio(); final _baseURL = 'https://accept.paymob.com/api/'; late String _apiKey; late int _integrationID; late int _iFrameID; late String _iFrameURL; late int _userTokenExpiration; /// Initializing PaymobPayment instance. Future initialize({ /// It is a unique identifier for the merchant which used to authenticate your requests calling any of Accept's API. /// from dashboard Select Settings -> Account Info -> API Key required String apiKey, /// from dashboard Select Developers -> Payment Integrations -> Online Card ID required int integrationID, /// from paymob Select Developers -> iframes required int iFrameID, /// The expiration time of this payment token in seconds. (The maximum is 3600 seconds which is an hour) int userTokenExpiration = 300, }) async { if (_isInitialized) { return true; } _dio.options.baseUrl = _baseURL; _dio.options.validateStatus = (status) => true; _apiKey = apiKey; _integrationID = integrationID; _iFrameID = iFrameID; _iFrameURL = 'https://accept.paymobsolutions.com/api/acceptance/iframes/$_iFrameID?payment_token='; _isInitialized = true; _userTokenExpiration = userTokenExpiration; return _isInitialized; } /// Get authentication token, which is valid for one hour from the creation time. Future _getAuthToken() async { try { final response = await _dio.post( 'auth/tokens', data: { 'api_key': _apiKey, }, ); return response.data['token']; } catch (e) { rethrow; } } /// At this step, you will register an order to Accept's database, so that you can pay for it later using a transaction Future _addOrder({ required String authToken, required String currency, required String amount, required List items, }) async { try { final response = await _dio.post( 'ecommerce/orders', data: { "auth_token": authToken, "delivery_needed": "false", "amount_cents": amount, "currency": currency, "items": items, }, ); return response.data['id']; } catch (e) { rethrow; } } /// At this step, you will obtain a payment_key token. This key will be used to authenticate your payment request. It will be also used for verifying your transaction request metadata. Future _getPurchaseToken({ required String authToken, required String currency, required int orderID, required String amount, required PaymobBillingData billingData, }) async { final response = await _dio.post( 'acceptance/payment_keys', data: { "auth_token": authToken, "amount_cents": amount, "expiration": _userTokenExpiration, "order_id": orderID, "billing_data": billingData, "currency": currency, "integration_id": _integrationID, "lock_order_when_paid": "false" }, ); final message = response.data['message']; if (message != null) { throw Exception(message); } return response.data['token']; } /// Proceed to pay with only calling this function. /// Opens a WebView at Paymob redirectedURL to accept user payment info. Future pay( { /// BuildContext for navigation to WebView required BuildContext context, /// Which Currency you would pay in. required String currency, /// Payment amount in cents EX: 20000 is an 200 EGP required String amountInCents, /// Optional Callback if you can use return result of pay function or use this callback void Function(PaymobResponse response)? onPayment, /// list of json objects contains the contents of the purchase. List? items, /// The billing data related to the customer related to this payment. PaymobBillingData? billingData}) async { if (!_isInitialized) { throw Exception( 'PaymobPayment is not initialized call:`PaymobPayment.instance.initialize`'); } final authToken = await _getAuthToken(); final orderID = await _addOrder( authToken: authToken, currency: currency, amount: amountInCents, items: items ?? [], ); final purchaseToken = await _getPurchaseToken( authToken: authToken, currency: currency, orderID: orderID, amount: amountInCents, billingData: billingData ?? PaymobBillingData(), ); if (context.mounted) { final response = await PaymobIFrame.show( context: context, redirectURL: _iFrameURL + purchaseToken, onPayment: onPayment, ); return response; } return null; } //51624 } class PaymobBillingData { String? email; String? firstName; String? lastName; String? phoneNumber; String? apartment; String? floor; String? street; String? building; String? postalCode; String? city; String? state; String? country; String? shippingMethod; PaymobBillingData({ this.email, this.firstName, this.lastName, this.phoneNumber, this.apartment, this.floor, this.street, this.building, this.postalCode, this.city, this.state, this.country, this.shippingMethod, }); Map toJson() { return { "email": box.read(BoxName.email) ?? box.read(BoxName.emailDriver), "first_name": EncryptionHelper.instance .decryptData(box.read(BoxName.name).toString().split(' ')[0]) .toString(), "last_name": EncryptionHelper.instance .decryptData(box.read(BoxName.name).toString().split(' ')[1]) .toString(), "phone_number": EncryptionHelper.instance.decryptData(box.read(BoxName.phone)), "apartment": apartment ?? "NA", "floor": floor ?? "NA", "building": building ?? "NA", "street": street ?? "NA", "postal_code": postalCode ?? "NA", "city": city ?? "NA", "state": state ?? "NA", "country": country ?? "NA", "shipping_method": box.read(BoxName.passengerID) ?? "NA", }; } } class PaymobIFrame extends StatefulWidget { const PaymobIFrame({ Key? key, required this.redirectURL, this.onPayment, }) : super(key: key); final String redirectURL; final void Function(PaymobResponse)? onPayment; static Future show({ required BuildContext context, required String redirectURL, void Function(PaymobResponse)? onPayment, }) => Navigator.of(context).push( MaterialPageRoute( builder: (context) { return PaymobIFrame( onPayment: onPayment, redirectURL: redirectURL, ); }, ), ); @override State createState() => _PaymobIFrameState(); } class _PaymobIFrameState extends State { WebViewController? controller; @override void initState() { controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate( NavigationDelegate( onNavigationRequest: (NavigationRequest request) { if (request.url.contains('txn_response_code') && request.url.contains('success') && request.url.contains('id')) { final params = _getParamFromURL(request.url); final response = PaymobResponse.fromJson(params); if (widget.onPayment != null) { widget.onPayment!(response); } Navigator.pop(context, response); return NavigationDecision.prevent; } return NavigationDecision.navigate; }, ), ) ..loadRequest(Uri.parse(widget.redirectURL)); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( body: controller == null ? const Center( child: CircularProgressIndicator.adaptive(), ) : SafeArea( child: WebViewWidget( controller: controller!, ), ), ); } Map _getParamFromURL(String url) { final uri = Uri.parse(url); Map data = {}; uri.queryParameters.forEach((key, value) { data[key] = value; }); return data; } } class PaymentScreen extends StatefulWidget { final String iframeUrl; const PaymentScreen({required this.iframeUrl, Key? key}) : super(key: key); @override State createState() => _PaymentScreenState(); } class _PaymentScreenState extends State { late final WebViewController _controller; @override void initState() { super.initState(); _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate(NavigationDelegate( onPageFinished: (url) { Log.print('url onPageFinished : ${url}'); if (url.contains("success")) { _fetchPaymentStatus(); // ✅ استدعاء الويب هوك بعد نجاح الدفع } else if (url.contains("failed")) { showCustomDialog( title: "Error".tr, message: 'Payment Failed'.tr, // يتم جلب رسالة الخطأ من الخادم isSuccess: false, ); } }, )) ..loadRequest(Uri.parse(widget.iframeUrl)); } // ✅ استدعاء الويب هوك بعد انتهاء الدفع Future _fetchPaymentStatus() async { final String userId = EncryptionHelper.instance .decryptData(box.read(BoxName.phoneWallet)); // ضع user_id الحقيقي final String apiUrl = AppLink.paymetVerifyPassenger; try { final response = await CRUD().getWallet(link: apiUrl, payload: { 'user_id': userId, 'passengerId': box.read(BoxName.passengerID), 'paymentMethod': 'visa-in', }); if (response != 'failure' && response != 'token_expired') { try { final jsonData = jsonDecode(response); if (jsonData['status'] == 'success') { // تأكد أن 'message' هو String وليس Map // final message = jsonData['message']; showCustomDialog( title: "Payment Status", message: jsonData['message'], // يتم جلب الرسالة من الخادم isSuccess: true, ); } else { showCustomDialog( title: "Error", message: jsonData['message'], // يتم جلب رسالة الخطأ من الخادم isSuccess: false, ); } } catch (e) { showCustomDialog( title: "Error", message: response, // يتم جلب رسالة الخطأ من الخادم isSuccess: false, ); } } else { showCustomDialog( title: "Error".tr, message: response, // يتم جلب رسالة الخطأ من الخادم isSuccess: false, ); } } catch (e) { showCustomDialog( title: "Error".tr, message: 'Server error'.tr, // يتم جلب رسالة الخطأ من الخادم isSuccess: false, ); } } void showCustomDialog({ required String title, required String message, required bool isSuccess, }) { showDialog( barrierDismissible: false, context: Get.context!, builder: (BuildContext context) { return AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.0), ), title: Row( children: [ Icon( isSuccess ? Icons.check_circle : Icons.error, color: isSuccess ? Colors.green : Colors.red, ), const SizedBox(width: 8), Text( title, style: TextStyle( color: isSuccess ? Colors.green : Colors.red, fontWeight: FontWeight.bold, ), ), ], ), content: Text( message, style: const TextStyle(fontSize: 16), ), actions: [ TextButton( onPressed: () { Navigator.pop(context); Navigator.pop(context); }, style: TextButton.styleFrom( backgroundColor: isSuccess ? Colors.green : Colors.red, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8.0), ), ), child: Text( "OK", style: const TextStyle(color: Colors.white), ), ), ], ); }, ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('إتمام الدفع')), body: WebViewWidget(controller: _controller), ); } }