// controllers/deposit_controller.dartimport 'dart:io';import 'package:firebase_messaging/firebase_messaging.dart';import 'package:fitfood_2f/api/repositories/transaction_repository.dart';import 'package:fitfood_2f/core/services/local_storage_service.dart';import 'package:fitfood_2f/features/deposit/models/deposit-request.dart';import 'package:fitfood_2f/features/deposit/models/payment-method.dart';import 'package:fitfood_2f/routes/route_names.dart';import 'package:fitfood_2f/theme/app_colors.dart';import 'package:flutter/material.dart';import 'package:flutter/services.dart';import 'package:get/get.dart';import 'package:permission_handler/permission_handler.dart';class DepositController extends GetxController { final TransactionRepository _transactionRepository = Get.put( TransactionRepository(), ); final LocalStorageService _storageService = Get.find<LocalStorageService>(); final RxString paymentUrl = ''.obs; final TextEditingController amountController = TextEditingController(); final RxList<PaymentMethod> paymentMethods = <PaymentMethod>[].obs; final RxInt selectedPaymentMethodId = (-1).obs; final RxBool isLoading = false.obs; final RxBool isProcessing = false.obs; final RxString errorMessage = ''.obs; final RxList<int> quickAmounts = [10000, 20000, 50000, 100000, 200000, 500000, 1000000].obs; final RxBool showWebView = false.obs; final RxString transactionId = ''.obs; @override void onInit() { super.onInit(); loadPaymentMethods(); } @override void onClose() { amountController.dispose(); super.onClose(); } Future<void> loadPaymentMethods() async { isLoading.value = true; errorMessage.value = ''; try { final response = await _transactionRepository.getPaymentMethods(); if (response.isSuccessed && response.resultObj != null) { paymentMethods.value = response.resultObj!; if (paymentMethods.isNotEmpty) { selectedPaymentMethodId.value = paymentMethods[0].id; } } else { errorMessage.value = response.message ?? 'Không thể tải phương thức thanh toán'; } } catch (e) { errorMessage.value = 'Đã xảy ra lỗi khi tải phương thức thanh toán'; } finally { isLoading.value = false; } } void selectPaymentMethod(int id) { selectedPaymentMethodId.value = id; } void selectQuickAmount(int amount) { amountController.text = formatCurrency(amount.toString()); } bool validateAmount() { final amountText = amountController.text.trim(); if (amountText.isEmpty) { errorMessage.value = 'Vui lòng nhập số tiền'; return false; } double? amount; try { // Xử lý chuỗi có dấu phẩy ngăn cách hàng nghìn amount = double.parse(amountText.replaceAll(RegExp(r'[^\d]'), '')); } catch (e) { errorMessage.value = 'Số tiền không hợp lệ'; return false; } try { amount = double.parse(amountText.replaceAll(RegExp(r'[^\d]'), '')); } catch (e) { errorMessage.value = 'Số tiền không hợp lệ'; return false; } if (amount < 10000) { errorMessage.value = 'Số tiền tối thiểu là 10,000đ'; return false; } if (amount > 100000000) { errorMessage.value = 'Số tiền tối đa là 100,000,000đ'; return false; } errorMessage.value = ''; return true; } Future<void> deposit() async { if (!validateAmount()) { return; } if (selectedPaymentMethodId.value < 0) { errorMessage.value = 'Vui lòng chọn phương thức thanh toán'; return; } isProcessing.value = true; errorMessage.value = ''; try { final userId = _storageService.getUser()?.id ?? ''; final amount = double.parse( amountController.text.replaceAll(RegExp(r'[^\d]'), ''), ); final request = DepositRequest( userId: userId, amount: amount, paymentMethod: selectedPaymentMethodId.value, ); final response = await _transactionRepository.depositMoney(request); if (response.isSuccessed && response.statusCode == 200 && response.resultObj != null) { // Kiểm tra xem có URL thanh toán không if (response.resultObj!.paymentUrl != null && response.resultObj!.paymentUrl!.isNotEmpty) { paymentUrl.value = response.resultObj!.paymentUrl!; transactionId.value = response.resultObj!.transactionId!; // Mở URL thanh toán openPaymentUrl(paymentUrl.value); } else { // Nếu không có URL thanh toán, chuyển đến trang thành công // navigateToSuccessScreen(amount); } } else { errorMessage.value = response.message ?? 'Nạp tiền không thành công'; } } catch (e) { errorMessage.value = 'Đã xảy ra lỗi khi nạp tiền'; } finally { isProcessing.value = false; } } Future<bool> checkAndRequestNotificationPermission() async { if (Platform.isIOS) { // Kiểm tra quyền notification chi tiết trên iOS final settings = await FirebaseMessaging.instance.getNotificationSettings(); if (settings.authorizationStatus == AuthorizationStatus.authorized || settings.authorizationStatus == AuthorizationStatus.provisional) { return true; } // Request quyền nếu chưa có final result = await FirebaseMessaging.instance.requestPermission( alert: true, badge: true, sound: true, ); return result.authorizationStatus == AuthorizationStatus.authorized || result.authorizationStatus == AuthorizationStatus.provisional; } else { // Android logic giữ nguyên var status = await Permission.notification.status; if (status.isGranted) return true; if (status.isDenied || status.isPermanentlyDenied) { // Show snackbar và mở settings return false; } final result = await Permission.notification.request(); return result.isGranted; } } Future<void> cancelDeposit() async { try { final response = await _transactionRepository.cancelDeposit( transactionId.value, ); if (response.isSuccessed) { // Get.back(); } else { Get.snackbar( 'Lỗi', response.message ?? 'Hủy giao dịch không thành công', ); } } catch (e) { Get.snackbar('Lỗi', 'Đã xảy ra lỗi khi hủy giao dịch'); } } Future<void> openPaymentUrl(String url) async { // Phương pháp 1: Mở trong trình duyệt bên ngoài // if (await canLaunch(url)) { // await launch(url); // // Sau khi mở URL, chúng ta có thể theo dõi trạng thái giao dịch // // bằng cách gọi API kiểm tra trạng thái theo định kỳ // startCheckingTransactionStatus(); // } else { // Phương pháp 2: Mở trong WebView nếu không thể mở trình duyệt showWebView.value = true; Get.toNamed(RouteNames.paymentWebView, arguments: {'url': url}); } String formatCurrency(String value) { if (value.isEmpty) return ''; // Xóa tất cả ký tự không phải số String numericValue = value.replaceAll(RegExp(r'[^\d]'), ''); // Định dạng với dấu phẩy ngăn cách hàng nghìn final formatted = numericValue.replaceAllMapped( RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]},', ); return formatted; }}// models/deposit_request.dartclass DepositRequest { final String userId; final double amount; final int paymentMethod; DepositRequest({ required this.userId, required this.amount, required this.paymentMethod, }); Map<String, dynamic> toJson() { return {'userId': userId, 'amount': amount, 'paymentMethod': paymentMethod}; }}class DepositResponse { String? paymentUrl; String? transactionId; DepositResponse({this.paymentUrl, this.transactionId}); DepositResponse.fromJson(Map<String, dynamic> json) { if (json["paymentUrl"] is String) { paymentUrl = json["paymentUrl"]; } if (json["transactionId"] is String) { transactionId = json["transactionId"]; } } Map<String, dynamic> toJson() { final Map<String, dynamic> data = <String, dynamic>{}; data["paymentUrl"] = paymentUrl; data["transactionId"] = transactionId; return data; }}// models/payment_method.dartclass PaymentMethod { final int id; final String name; final String? icon; PaymentMethod({required this.id, required this.name, this.icon}); factory PaymentMethod.fromJson(Map<String, dynamic> json) { return PaymentMethod( id: json['id'], name: json['name'], icon: null, // Có thể bổ sung sau nếu API trả về icon ); }}// screens/deposit_failure_screen.dartimport 'package:flutter/material.dart';import 'package:get/get.dart';import '../../../theme/app_colors.dart';import '../../../routes/route_names.dart';class DepositFailureScreen extends StatelessWidget { final double amount = Get.arguments['amount'] ?? 0.0; final String paymentMethod = Get.arguments['paymentMethod'] ?? ''; final String errorMessage = Get.arguments['errorMessage'] ?? 'Giao dịch không thành công'; @override Widget build(BuildContext context) { final Size screenSize = MediaQuery.of(context).size; final bool isTablet = screenSize.shortestSide >= 600; return Scaffold( body: SafeArea( child: Center( child: Padding( padding: EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Failure icon Container( width: isTablet ? 180 : 140, height: isTablet ? 180 : 140, decoration: BoxDecoration( color: Colors.red.withOpacity(0.1), shape: BoxShape.circle, ), child: Icon( Icons.error, color: Colors.red, size: isTablet ? 100 : 80, ), ), SizedBox(height: 32), // Failure message Text( 'Nạp tiền không thành công!', style: TextStyle( fontSize: isTablet ? 32 : 28, fontWeight: FontWeight.bold, color: AppColors.textPrimary, ), ), SizedBox(height: 16), Text( 'Giao dịch nạp tiền của bạn đã thất bại', textAlign: TextAlign.center, style: TextStyle( fontSize: isTablet ? 20 : 18, color: AppColors.textGray, ), ), SizedBox(height: 8), Text( '${amount.toStringAsFixed(0).replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]},')}đ', textAlign: TextAlign.center, style: TextStyle( fontSize: isTablet ? 24 : 20, fontWeight: FontWeight.bold, color: Colors.red, ), ), SizedBox(height: 16), // Error message Container( padding: EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.red.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.red.withOpacity(0.3)), ), child: Text( errorMessage, textAlign: TextAlign.center, style: TextStyle( fontSize: isTablet ? 16 : 14, color: Colors.red.shade700, fontWeight: FontWeight.w500, ), ), ), SizedBox(height: 24), // Transaction details Container( padding: EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey.shade200), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.1), blurRadius: 10, offset: Offset(0, 4), ), ], ), child: Column( children: [ _buildDetailRow( label: 'Số tiền', value: '${amount.toStringAsFixed(0).replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]},')}đ', isTablet: isTablet, valueColor: Colors.red, ), Divider(height: 24), _buildDetailRow( label: 'Phương thức', value: paymentMethod, isTablet: isTablet, ), Divider(height: 24), _buildDetailRow( label: 'Thời gian', value: _getCurrentDateTime(), isTablet: isTablet, ), Divider(height: 24), _buildDetailRow( label: 'Trạng thái', value: 'Thất bại', isTablet: isTablet, valueColor: Colors.red, ), ], ), ), Spacer(), // Action buttons Column( children: [ // Try again button Container( width: double.infinity, decoration: BoxDecoration( gradient: LinearGradient( colors: [ AppColors.primaryButtonGredientTopLeft, AppColors.primaryButtonGredientBottomRight, ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(12), ), child: ElevatedButton( onPressed: () { Get.toNamed(RouteNames.deposit); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.transparent, shadowColor: Colors.transparent, padding: EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: Text( 'Thử lại', style: TextStyle( fontSize: isTablet ? 18 : 16, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), SizedBox(height: 12), // Return to home button Container( width: double.infinity, child: OutlinedButton( onPressed: () { Get.offAllNamed(RouteNames.home); }, style: OutlinedButton.styleFrom( padding: EdgeInsets.symmetric(vertical: 16), side: BorderSide(color: AppColors.primary), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: Text( 'Quay về trang chủ', style: TextStyle( fontSize: isTablet ? 18 : 16, fontWeight: FontWeight.bold, color: AppColors.primary, ), ), ), ), ], ), ], ), ), ), ), ); } Widget _buildDetailRow({ required String label, required String value, required bool isTablet, Color? valueColor, }) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: TextStyle( fontSize: isTablet ? 16 : 14, color: AppColors.textGray, ), ), Text( value, style: TextStyle( fontSize: isTablet ? 16 : 14, fontWeight: FontWeight.bold, color: valueColor ?? AppColors.textPrimary, ), ), ], ); } String _getCurrentDateTime() { final now = DateTime.now(); final day = now.day.toString().padLeft(2, '0'); final month = now.month.toString().padLeft(2, '0'); final year = now.year.toString(); final hour = now.hour.toString().padLeft(2, '0'); final minute = now.minute.toString().padLeft(2, '0'); return '$day/$month/$year $hour:$minute'; }}// screens/deposit_screen.dartimport 'package:fitfood_2f/core/helper/format-utils.dart';import 'package:fitfood_2f/features/components/top_navbar.dart';import 'package:fitfood_2f/features/deposit/controllers/deposit-controller.dart';import 'package:fitfood_2f/features/deposit/models/payment-method.dart';import 'package:fitfood_2f/features/deposit/widgets/currency-input-formatter.dart';import 'package:flutter/material.dart';import 'package:flutter/services.dart';import 'package:get/get.dart';import '../../../theme/app_colors.dart';class DepositScreen extends StatelessWidget { final DepositController controller = Get.put(DepositController()); @override Widget build(BuildContext context) { final Size screenSize = MediaQuery.of(context).size; final bool isTablet = screenSize.shortestSide >= 600; return Scaffold( backgroundColor: Colors.grey.shade50, appBar: CustomAppBar( title: 'Nạp FF Point', onBack: () => Navigator.pop(context, {'needRefresh': true}), ), body: Obx( () => controller.isLoading.value ? Center( child: CircularProgressIndicator(color: AppColors.primary), ) : SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Container( width: double.infinity, padding: EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Nhập số tiền cần nạp', style: TextStyle( fontSize: isTablet ? 18 : 16, fontWeight: FontWeight.bold, color: AppColors.textPrimary, ), ), Text( 'Số tiền tối thiểu là 10,000đ và tối đa 100,000,000đ', style: TextStyle( fontSize: isTablet ? 16 : 14, color: AppColors.textPrimary, fontStyle: FontStyle.italic, ), ), SizedBox(height: 16), // Amount input Container( padding: EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.shade300), ), child: Row( children: [ Text( 'đ', style: TextStyle( fontSize: isTablet ? 24 : 20, fontWeight: FontWeight.bold, color: AppColors.primary, ), ), SizedBox(width: 8), Expanded( child: TextField( controller: controller.amountController, keyboardType: TextInputType.number, style: TextStyle( fontSize: isTablet ? 24 : 20, fontWeight: FontWeight.bold, color: AppColors.textPrimary, ), decoration: InputDecoration( border: InputBorder.none, hintText: '0', hintStyle: TextStyle( color: Colors.grey.shade400, ), ), inputFormatters: [ CurrencyInputFormatter( min: 0, max: 100000000, ), ], ), ), ], ), ), SizedBox(height: 8), // Error message Obx( () => controller.errorMessage.value.isNotEmpty ? Padding( padding: EdgeInsets.only(top: 8), child: Text( controller.errorMessage.value, style: TextStyle( color: Colors.red, fontSize: 14, ), ), ) : SizedBox.shrink(), ), SizedBox(height: 16), // Quick amount options Text( 'Chọn nhanh', style: TextStyle( fontSize: isTablet ? 16 : 14, fontWeight: FontWeight.w500, color: AppColors.textGray, ), ), SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: controller.quickAmounts.map((amount) { return InkWell( onTap: () => controller.selectQuickAmount( amount, ), child: Container( padding: EdgeInsets.symmetric( horizontal: 16, vertical: 10, ), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular( 8, ), border: Border.all( color: AppColors.primary .withOpacity(0.3), ), ), child: Text( '${amount.toString().replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]},')}đ', style: TextStyle( fontSize: isTablet ? 16 : 14, color: AppColors.primary, fontWeight: FontWeight.w500, ), ), ), ); }).toList(), ), ], ), ), SizedBox(height: 16), // Payment methods Container( width: double.infinity, padding: EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Chọn phương thức thanh toán', style: TextStyle( fontSize: isTablet ? 18 : 16, fontWeight: FontWeight.bold, color: AppColors.textPrimary, ), ), SizedBox(height: 16), // Payment method list ...controller.paymentMethods.map((method) { return Obx( () => _buildPaymentMethodItem( method: method, isSelected: controller .selectedPaymentMethodId .value == method.id, onTap: () => controller.selectPaymentMethod( method.id, ), isTablet: isTablet, ), ); }).toList(), ], ), ), SizedBox(height: 24), // Deposit button Padding( padding: EdgeInsets.symmetric(horizontal: 24), child: Obx( () => ElevatedButton( onPressed: controller.isProcessing.value ? null : controller.deposit, style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, padding: EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), minimumSize: Size(double.infinity, 56), ), child: controller.isProcessing.value ? SizedBox( width: 24, height: 24, child: CircularProgressIndicator( color: Colors.white, strokeWidth: 2, ), ) : Text( 'Nạp tiền', style: TextStyle( fontSize: isTablet ? 18 : 16, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), ), SizedBox(height: 24), // Note Padding( padding: EdgeInsets.symmetric(horizontal: 24), child: Container( padding: EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.blue.shade100), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( Icons.info_outline, color: Colors.blue, size: 20, ), SizedBox(width: 12), Expanded( child: Text( 'FF Point là đơn vị tiền tệ trong ứng dụng. 1 FF Point = 1 VNĐ. Số dư FF Point không thể rút ra ngoài.', style: TextStyle( fontSize: isTablet ? 14 : 13, color: Colors.blue.shade800, ), ), ), ], ), ), ), SizedBox(height: 40), ], ), ), ), ); } Widget _buildPaymentMethodItem({ required PaymentMethod method, required bool isSelected, required VoidCallback onTap, required bool isTablet, }) { // Map các phương thức thanh toán với icon tương ứng final Map<String, IconData> paymentIcons = { 'Ví ZaloPay': Icons.account_balance_wallet, 'Visa, Master, JCB': Icons.credit_card, 'ATM nội địa': Icons.local_atm, 'Ví FF': Icons.account_balance_wallet, 'VietQR': Icons.qr_code, }; IconData icon = Icons.payment; for (var key in paymentIcons.keys) { if (method.name.contains(key)) { icon = paymentIcons[key]!; break; } } return GestureDetector( onTap: onTap, child: Container( margin: EdgeInsets.only(bottom: 12), padding: EdgeInsets.all(16), decoration: BoxDecoration( color: isSelected ? AppColors.primary.withOpacity(0.05) : Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all( color: isSelected ? AppColors.primary : Colors.grey.shade200, width: isSelected ? 2 : 1, ), ), child: Row( children: [ Container( padding: EdgeInsets.all(10), decoration: BoxDecoration( color: isSelected ? AppColors.primary.withOpacity(0.1) : Colors.grey.shade100, borderRadius: BorderRadius.circular(8), ), child: Icon( icon, color: isSelected ? AppColors.primary : Colors.grey.shade700, size: isTablet ? 24 : 20, ), ), SizedBox(width: 16), Expanded( child: Text( method.name, style: TextStyle( fontSize: isTablet ? 16 : 14, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, color: isSelected ? AppColors.primary : AppColors.textPrimary, ), ), ), if (isSelected) Icon( Icons.check_circle, color: AppColors.primary, size: isTablet ? 24 : 20, ), ], ), ), ); }}// screens/deposit_success_screen.dartimport 'package:flutter/material.dart';import 'package:get/get.dart';import '../../../theme/app_colors.dart';import '../../../routes/route_names.dart';class DepositSuccessScreen extends StatelessWidget { final double amount = Get.arguments['amount'] ?? 0.0; final String paymentMethod = Get.arguments['paymentMethod'] ?? ''; @override Widget build(BuildContext context) { final Size screenSize = MediaQuery.of(context).size; final bool isTablet = screenSize.shortestSide >= 600; return Scaffold( body: SafeArea( child: Center( child: Padding( padding: EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Success icon Container( width: isTablet ? 180 : 140, height: isTablet ? 180 : 140, decoration: BoxDecoration( color: Colors.green.withOpacity(0.1), shape: BoxShape.circle, ), child: Icon( Icons.check_circle, color: Colors.green, size: isTablet ? 100 : 80, ), ), SizedBox(height: 32), // Success message Text( 'Nạp tiền thành công!', style: TextStyle( fontSize: isTablet ? 32 : 28, fontWeight: FontWeight.bold, color: AppColors.textPrimary, ), ), SizedBox(height: 16), Text( 'Bạn đã nạp thành công', textAlign: TextAlign.center, style: TextStyle( fontSize: isTablet ? 20 : 18, color: AppColors.textGray, ), ), SizedBox(height: 8), Text( '${amount.toStringAsFixed(0).replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]},')}đ', textAlign: TextAlign.center, style: TextStyle( fontSize: isTablet ? 24 : 20, fontWeight: FontWeight.bold, color: AppColors.primary, ), ), SizedBox(height: 24), // Transaction details Container( padding: EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey.shade200), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.1), blurRadius: 10, offset: Offset(0, 4), ), ], ), child: Column( children: [ _buildDetailRow( label: 'Số tiền', value: '${amount.toStringAsFixed(0).replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]},')}đ', isTablet: isTablet, valueColor: AppColors.primary, ), Divider(height: 24), _buildDetailRow( label: 'Phương thức', value: paymentMethod, isTablet: isTablet, ), Divider(height: 24), _buildDetailRow( label: 'Thời gian', value: _getCurrentDateTime(), isTablet: isTablet, ), Divider(height: 24), _buildDetailRow( label: 'Trạng thái', value: 'Thành công', isTablet: isTablet, valueColor: Colors.green, ), ], ), ), Spacer(), // Return button Container( width: double.infinity, decoration: BoxDecoration( gradient: LinearGradient( colors: [ AppColors.primaryButtonGredientTopLeft, AppColors.primaryButtonGredientBottomRight, ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(12), ), child: ElevatedButton( onPressed: () { Get.offAllNamed(RouteNames.home); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.transparent, shadowColor: Colors.transparent, padding: EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: Text( 'Quay về trang chủ', style: TextStyle( fontSize: isTablet ? 18 : 16, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), ], ), ), ), ), ); } Widget _buildDetailRow({ required String label, required String value, required bool isTablet, Color? valueColor, }) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: TextStyle( fontSize: isTablet ? 16 : 14, color: AppColors.textGray, ), ), Text( value, style: TextStyle( fontSize: isTablet ? 16 : 14, fontWeight: FontWeight.bold, color: valueColor ?? AppColors.textPrimary, ), ), ], ); } String _getCurrentDateTime() { final now = DateTime.now(); final day = now.day.toString().padLeft(2, '0'); final month = now.month.toString().padLeft(2, '0'); final year = now.year.toString(); final hour = now.hour.toString().padLeft(2, '0'); final minute = now.minute.toString().padLeft(2, '0'); return '$day/$month/$year $hour:$minute'; }}import 'dart:async';import 'dart:io';import 'package:fitfood_2f/features/deposit/controllers/deposit-controller.dart';import 'package:flutter/material.dart';import 'package:get/get.dart';import 'package:webview_flutter/webview_flutter.dart';import 'package:webview_flutter_android/webview_flutter_android.dart';import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';import 'package:url_launcher/url_launcher.dart';import '../../../theme/app_colors.dart';class PaymentWebViewScreen extends StatefulWidget { @override _PaymentWebViewScreenState createState() => _PaymentWebViewScreenState();}class _PaymentWebViewScreenState extends State<PaymentWebViewScreen> { final String url = Get.arguments['url']; late WebViewController _webViewController; bool isLoading = true; final DepositController _depositController = Get.find(); Timer? _autoCloseTimer; @override void initState() { super.initState(); _initializeWebViewController(); } @override void dispose() { _autoCloseTimer?.cancel(); super.dispose(); } void _initializeWebViewController() { // Tạo WebViewController với cấu hình phù hợp cho từng platform late final PlatformWebViewControllerCreationParams params; if (WebViewPlatform.instance is AndroidWebViewPlatform) { params = AndroidWebViewControllerCreationParams(); } else if (WebViewPlatform.instance is WebKitWebViewPlatform) { params = WebKitWebViewControllerCreationParams( allowsInlineMediaPlayback: true, mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{}, ); } else { params = const PlatformWebViewControllerCreationParams(); } _webViewController = WebViewController.fromPlatformCreationParams(params); // Cấu hình WebViewController _webViewController ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setBackgroundColor(const Color(0x00000000)) ..setNavigationDelegate( NavigationDelegate( onProgress: (int progress) { // Cập nhật progress nếu cần }, onPageStarted: (String url) { setState(() { isLoading = true; }); // Kiểm tra URL callback từ backend if (url.contains('/cancel') || url.contains('/success')) { _handlePaymentCallback(url); } }, onPageFinished: (String url) { setState(() { isLoading = false; }); // Inject JavaScript để setup bridge cho Android và iOS _setupJavaScriptBridge(); }, onWebResourceError: (WebResourceError error) { debugPrint('WebView error: ${error.description}'); }, onNavigationRequest: (NavigationRequest request) { // Xử lý các URL đặc biệt if (request.url.startsWith('zalopay:')) { // Mở ứng dụng ZaloPay nếu được cài đặt _launchURL(request.url); return NavigationDecision.prevent; } return NavigationDecision.navigate; }, ), ) ..loadRequest(Uri.parse(url)); // Cấu hình riêng cho Android if (_webViewController.platform is AndroidWebViewController) { AndroidWebViewController.enableDebugging(true); (_webViewController.platform as AndroidWebViewController) .setMediaPlaybackRequiresUserGesture(false); // Setup JavaScript interface cho Android _webViewController.addJavaScriptChannel( 'PaymentHandler', onMessageReceived: (JavaScriptMessage message) { _handleJavaScriptMessage(message.message); }, ); } } void _setupJavaScriptBridge() { // Inject JavaScript để setup bridge String jsCode = ''' // Setup bridge cho Android window.Android = { paymentSuccess: function() { PaymentHandler.postMessage('success'); }, paymentCancelled: function() { PaymentHandler.postMessage('cancelled'); } }; // Setup bridge cho iOS (nếu cần) window.webkit = window.webkit || {}; window.webkit.messageHandlers = window.webkit.messageHandlers || {}; window.webkit.messageHandlers.paymentHandler = { postMessage: function(message) { PaymentHandler.postMessage(message); } }; '''; _webViewController.runJavaScript(jsCode); } void _handleJavaScriptMessage(String message) { Get.back(); } void _handlePaymentCallback(String url) { // Cancel timer cũ nếu có _autoCloseTimer?.cancel(); // Kiểm tra URL để xác định trạng thái và auto close if (url.contains('/success')) { // Trang thành công - tự động đóng sau 10s _autoCloseTimer = Timer(Duration(seconds: 10), () { Get.back(); // Không cần gọi handlePaymentSuccess vì BE đã xử lý rồi }); } else if (url.contains('/cancel')) { // Trang hủy - tự động đóng sau 10s _autoCloseTimer = Timer(Duration(seconds: 10), () { Get.back(); // Không cần gọi cancelDeposit vì BE đã xử lý rồi }); } } Future<void> _launchURL(String url) async { try { final Uri uri = Uri.parse(url); if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); } else { debugPrint('Could not launch $url'); } } catch (e) { debugPrint('Error launching URL: $e'); } } @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () async { _showExitConfirmDialog(); return false; }, child: Scaffold( appBar: AppBar( title: Text( 'Thanh toán', style: TextStyle( color: AppColors.textPrimary, fontWeight: FontWeight.bold, ), ), centerTitle: true, backgroundColor: Colors.white, elevation: 0, iconTheme: IconThemeData(color: AppColors.textPrimary), leading: IconButton( icon: Icon(Icons.arrow_back), onPressed: () { _showExitConfirmDialog(); }, ), ), body: Stack( children: [ WebViewWidget(controller: _webViewController), if (isLoading) Center( child: CircularProgressIndicator(color: AppColors.primary), ), ], ), ), ); } void _showExitConfirmDialog() { showDialog( context: context, builder: (context) => AlertDialog( title: Text('Xác nhận'), content: Text('Bạn có chắc muốn hủy thanh toán này không?'), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); // Đóng dialog }, child: Text('Tiếp tục thanh toán'), ), ElevatedButton( onPressed: () { Navigator.of(context).pop(); // Đóng dialog _autoCloseTimer?.cancel(); _depositController.cancelDeposit(); Get.back(); // Chỉ cần back, không cần xử lý logic }, style: ElevatedButton.styleFrom(backgroundColor: Colors.red), child: Text('Hủy thanh toán'), ), ], ), ); }}import 'package:flutter/services.dart';import 'package:intl/intl.dart';class CurrencyInputFormatter extends TextInputFormatter { final int min; final int max; CurrencyInputFormatter({this.min = 0, this.max = 100000000}); final _formatter = NumberFormat("#,###"); @override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue, ) { String digits = newValue.text.replaceAll(RegExp(r'[^\d]'), ''); // Chặn khi chưa nhập gì if (digits.isEmpty) return newValue.copyWith(text: ''); int value = int.tryParse(digits) ?? 0; // Giới hạn giá trị if (value < min) value = min; if (value > max) value = max; String newText = _formatter.format(value); // Tính lại vị trí con trỏ int selectionIndex = newText.length; return TextEditingValue( text: newText, selection: TextSelection.collapsed(offset: selectionIndex), ); }}integrate Apple In-App Purchase (IAP) update all code in dart flutter
I'll help you integrate Apple In-App Purchase (IAP) into your existing Flutter deposit system. Here's the complete updated code with IAP integration:
PaymentMethod with type enumDepositRequest with IAP transaction data