import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:razorpay_flutter/razorpay_flutter.dart'; import 'package:upi_india/upi_india.dart'; import 'package:bookatanker/common/settings.dart'; class CreditTopUpConfirmScreen extends StatefulWidget { final String supplierName; final String supplierId; const CreditTopUpConfirmScreen({ super.key, required this.supplierName, required this.supplierId, }); @override State createState() => _CreditTopUpConfirmScreenState(); } class _CreditTopUpConfirmScreenState extends State { final TextEditingController amountCtrl = TextEditingController(text: "2000"); bool isPaying = false; String? errorText; // UPI late UpiIndia _upiIndia; List _apps = []; UpiApp? _selectedUpiApp; // Razorpay Razorpay? _razorpay; @override void initState() { super.initState(); _upiIndia = UpiIndia(); _loadUpiApps(); _razorpay = Razorpay(); _razorpay!.on(Razorpay.EVENT_PAYMENT_SUCCESS, _onRazorpaySuccess); _razorpay!.on(Razorpay.EVENT_PAYMENT_ERROR, _onRazorpayError); _razorpay!.on(Razorpay.EVENT_EXTERNAL_WALLET, _onRazorpayExternalWallet); } @override void dispose() { amountCtrl.dispose(); _razorpay?.clear(); super.dispose(); } Future _loadUpiApps() async { try { final apps = await _upiIndia.getAllUpiApps(mandatoryTransactionId: false); if (!mounted) return; setState(() { _apps = apps; // Prefer PhonePe if exists else first _selectedUpiApp = _apps.firstWhere( (a) => a.name.toLowerCase().contains("phonepe"), orElse: () => _apps.isNotEmpty ? _apps.first : UpiApp.bhim, ); }); } catch (_) { // Ignore, we will show Razorpay option anyway } } double _readAmount() { final raw = amountCtrl.text.trim().replaceAll(",", ""); final v = double.tryParse(raw) ?? 0; return v; } bool _validateAmount() { final amt = _readAmount(); if (amt < 1) { setState(() => errorText = "Enter a valid amount"); return false; } if (amt > 1000000) { setState(() => errorText = "Amount too high"); return false; } setState(() => errorText = null); return true; } Future _payViaSelectedUpiApp() async { if (!_validateAmount()) return; if (_selectedUpiApp == null) { setState(() => errorText = "No UPI app found. Use Razorpay option."); return; } final amt = _readAmount(); setState(() { isPaying = true; errorText = null; }); try { // ๐Ÿ”ฅ Use YOUR business UPI ID here (store / supplier / platform UPI) // If you want supplier-wise UPI ID, pass it in widget. const receiverUpiId = "9912686262@xyz"; const receiverName = "Water Management"; final txnRef = "TOPUP_${DateTime.now().millisecondsSinceEpoch}"; final res = await _upiIndia.startTransaction( app: _selectedUpiApp!, receiverUpiId: receiverUpiId, receiverName: receiverName, transactionRefId: txnRef, transactionNote: "Credit TopUp", amount: amt, ); if (!mounted) return; // โœ… UPI result handling final status = (res.status ?? "").toLowerCase(); if (status == UpiPaymentStatus.SUCCESS.toLowerCase()) { // Save in your backend await AppSettings.addAdvanceTopUp( widget.supplierId, AppSettings.customerId, amt, "upi_${_selectedUpiApp!.name.toLowerCase()}", gatewayTxnId: res.transactionId ?? txnRef, ); if (!mounted) return; Navigator.pop(context, true); } else if (status == UpiPaymentStatus.SUBMITTED.toLowerCase()) { setState(() => errorText = "Payment pending. Please check in your UPI app."); } else { setState(() => errorText = "Payment failed or cancelled."); } } catch (e) { if (!mounted) return; setState(() => errorText = "Payment failed. Try again."); } finally { if (mounted) setState(() => isPaying = false); } } /// โœ… Razorpay (Recommended for full PhonePe/cards/netbanking etc.) /// Backend creates order_id securely, then open checkout. Future _payViaRazorpay() async { if (!_validateAmount()) return; final amt = _readAmount(); setState(() { isPaying = true; errorText = null; }); try { // 1) Create Razorpay order from backend final orderResp = await AppSettings.createRazorpayOrder( amount: amt, supplierId: widget.supplierId, customerId: AppSettings.customerId, ); final decoded = jsonDecode(orderResp); final orderId = decoded["order_id"]; // backend should return this final keyId = decoded["key_id"]; // backend may return key_id or keep static in app if (orderId == null || keyId == null) { throw Exception("Invalid order response"); } final options = { 'key': keyId, 'amount': (amt * 100).round(), // paise 'name': widget.supplierName, 'description': 'Credit Top Up', 'order_id': orderId, 'retry': {'enabled': true, 'max_count': 1}, 'prefill': { 'contact': AppSettings.phoneNumber ?? '', 'email': AppSettings.email ?? '', }, 'theme': {'color': '#1D7AFC'}, // 'external': {'wallets': ['paytm']} // optional }; _razorpay?.open(options); } catch (e) { if (!mounted) return; setState(() { isPaying = false; errorText = "Unable to start payment. Try again."; }); } } // Razorpay callbacks Future _onRazorpaySuccess(PaymentSuccessResponse response) async { final amt = _readAmount(); try { // 2) Verify payment on backend (signature verification) await AppSettings.verifyRazorpayPayment( razorpayPaymentId: response.paymentId ?? "", razorpayOrderId: response.orderId ?? "", razorpaySignature: response.signature ?? "", ); // 3) Save topup await AppSettings.addAdvanceTopUp( widget.supplierId, AppSettings.customerId, amt, "razorpay", gatewayTxnId: response.paymentId ?? "", ); if (!mounted) return; Navigator.pop(context, true); } catch (e) { if (!mounted) return; setState(() { errorText = "Payment captured but verification failed. Contact support."; isPaying = false; }); } } void _onRazorpayError(PaymentFailureResponse response) { if (!mounted) return; setState(() { isPaying = false; errorText = "Payment failed. Please try again."; }); } void _onRazorpayExternalWallet(ExternalWalletResponse response) { // optional } @override Widget build(BuildContext context) { final amount = _readAmount(); return Scaffold( backgroundColor: Colors.white, appBar: AppBar( elevation: 0, backgroundColor: Colors.white, leading: IconButton( icon: const Icon(Icons.close, color: Colors.black), onPressed: () => Navigator.pop(context), ), ), body: Padding( padding: const EdgeInsets.all(24), child: Column( children: [ const SizedBox(height: 24), AnimatedSwitcher( duration: const Duration(milliseconds: 250), child: Text( widget.supplierName, key: ValueKey(widget.supplierName), style: fontTextStyle(16, Colors.black, FontWeight.w600), ), ), const SizedBox(height: 6), Text("Credit Account ยท Top Up", style: fontTextStyle(12, Colors.grey, FontWeight.w400)), const SizedBox(height: 24), // โœ… Editable amount AnimatedContainer( duration: const Duration(milliseconds: 250), padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8), decoration: BoxDecoration( color: const Color(0xFFEFF4FF), borderRadius: BorderRadius.circular(16), border: Border.all( color: errorText != null ? Colors.red.withOpacity(0.5) : Colors.transparent, ), ), child: TextField( controller: amountCtrl, keyboardType: TextInputType.number, textAlign: TextAlign.center, onChanged: (_) { if (errorText != null) setState(() => errorText = null); setState(() {}); // animate amount changes }, style: fontTextStyle(26, const Color(0xFF1D7AFC), FontWeight.w700), decoration: const InputDecoration( border: InputBorder.none, prefixText: "โ‚น", ), ), ), const SizedBox(height: 8), Text("What is it for?", style: fontTextStyle(12, Colors.grey, FontWeight.w400)), if (errorText != null) ...[ const SizedBox(height: 12), AnimatedOpacity( opacity: 1, duration: const Duration(milliseconds: 250), child: Text(errorText!, style: fontTextStyle(12, Colors.red, FontWeight.w500)), ), ], const SizedBox(height: 18), // โœ… UPI App Selector if (_apps.isNotEmpty) ...[ Align( alignment: Alignment.centerLeft, child: Text("Pay using UPI App", style: fontTextStyle(12, const Color(0xFF3B3B3B), FontWeight.w600)), ), const SizedBox(height: 10), SizedBox( height: 56, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: _apps.length, separatorBuilder: (_, __) => const SizedBox(width: 10), itemBuilder: (_, i) { final app = _apps[i]; final selected = _selectedUpiApp?.name == app.name; return GestureDetector( onTap: () => setState(() => _selectedUpiApp = app), child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: selected ? const Color(0xFF1D7AFC).withOpacity(0.12) : Colors.white, borderRadius: BorderRadius.circular(14), border: Border.all( color: selected ? const Color(0xFF1D7AFC) : const Color(0xFFE0E0E0), ), ), child: Row( children: [ if (app.icon != null) Image.memory(app.icon!, width: 22, height: 22), const SizedBox(width: 8), Text(app.name, style: fontTextStyle(12, const Color(0xFF3B3B3B), FontWeight.w500)), ], ), ), ); }, ), ), ], const Spacer(), // โœ… Buttons (Animated) AnimatedSwitcher( duration: const Duration(milliseconds: 250), child: isPaying ? SizedBox( key: const ValueKey("loading"), width: double.infinity, height: 48, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF1D7AFC), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), ), onPressed: null, child: const SizedBox( width: 22, height: 22, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ), ), ) : Column( key: const ValueKey("buttons"), children: [ // โœ… Confirm = UPI app selected SizedBox( width: double.infinity, height: 48, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF1D7AFC), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), ), onPressed: (_apps.isNotEmpty) ? _payViaSelectedUpiApp : null, child: Text( _apps.isNotEmpty ? "Confirm (UPI: ${_selectedUpiApp?.name ?? ''})" : "Confirm (UPI not available)", style: fontTextStyle(14, Colors.white, FontWeight.w600), ), ), ), const SizedBox(height: 10), // โœ… Razorpay = best for PhonePe/cards/netbanking SizedBox( width: double.infinity, height: 48, child: OutlinedButton( style: OutlinedButton.styleFrom( side: const BorderSide(color: Color(0xFF1D7AFC)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), ), onPressed: _payViaRazorpay, child: Text( "Pay via Razorpay / PhonePe", style: fontTextStyle(14, const Color(0xFF1D7AFC), FontWeight.w600), ), ), ), const SizedBox(height: 10), Text( "Amount: โ‚น${amount.toStringAsFixed(0)}", style: fontTextStyle(10, const Color(0xFF757575), FontWeight.w400), ), ], ), ), ], ), ), ); } }