import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:provider/provider.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/app/constants/sc_screen.dart'; import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/app_localizations.dart'; import 'package:yumi/modules/wallet/recharge/recharge_method_bottom_sheet.dart'; import 'package:yumi/modules/wallet/wallet_route.dart'; import 'package:yumi/services/auth/user_profile_manager.dart'; import 'package:yumi/services/payment/apple_payment_manager.dart'; import 'package:yumi/services/payment/google_payment_manager.dart'; import 'package:yumi/services/payment/mifa_pay_manager.dart'; import 'package:yumi/shared/business_logic/models/res/sc_mifa_pay_res.dart'; import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; import 'package:yumi/ui_kit/components/sc_tts.dart'; import 'package:yumi/ui_kit/components/text/sc_text.dart'; class RechargePage extends StatefulWidget { const RechargePage({super.key}); @override State createState() => _RechargePageState(); } class _RechargePageState extends State { static const Color _accentGoldColor = Color(0xffF5C550); static const Color _surfaceTextColor = Colors.white; static const Color _surfaceSubtleTextColor = Color(0xCCFFFFFF); late final List _methodOptions; RechargeMethodType _selectedMethodType = RechargeMethodType.nativeStore; @override void initState() { super.initState(); _methodOptions = _buildMethodOptions(); Provider.of(context, listen: false).balance(); Provider.of(context, listen: false).initialize(); if (Platform.isAndroid) { Provider.of( context, listen: false, ).initializePaymentProcessor(context); } else if (Platform.isIOS) { Provider.of( context, listen: false, ).initializePaymentProcessor(context); } } @override Widget build(BuildContext context) { final MiFaPayManager miFaPayManager = context.watch(); return Stack( children: [ Image.asset( SCGlobalConfig.businessLogicStrategy.getRechargePageBackgroundImage(), width: ScreenUtil().screenWidth, fit: BoxFit.fill, ), Scaffold( backgroundColor: SCGlobalConfig.businessLogicStrategy .getRechargePageScaffoldBackgroundColor(), resizeToAvoidBottomInset: false, appBar: SocialChatStandardAppBar( title: SCAppLocalizations.of(context)!.recharge, actions: [ GestureDetector( onTap: _showRecordDialog, child: Container( margin: EdgeInsetsDirectional.only(end: 15.w), child: Image.asset( SCGlobalConfig.businessLogicStrategy .getRechargePageRecordIcon(), width: 24.w, height: 24.w, ), ), ), ], ), body: SafeArea( top: false, child: Column( children: [ _buildWalletSection(), SizedBox(height: 36.w), Expanded( child: Padding( padding: EdgeInsets.fromLTRB(12.w, 8.w, 12.w, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildMethodSection(miFaPayManager), SizedBox(height: 14.w), Expanded( child: _selectedMethodType == RechargeMethodType.nativeStore ? _buildNativeProductList() : _buildMiFaPayContent(miFaPayManager), ), if (_selectedMethodType == RechargeMethodType.nativeStore && Platform.isIOS) ...[ SizedBox(height: 12.w), _buildFooterButton( title: SCAppLocalizations.of( context, )!.restorePurchases, onTap: () { Provider.of( context, listen: false, ).recoverTransactions(); }, ), ], SizedBox(height: 12.w), _buildFooterButton( title: _selectedMethodType == RechargeMethodType.nativeStore ? SCAppLocalizations.of(context)!.recharge : _buildMiFaPayPrimaryTitle(miFaPayManager), onTap: () { if (_selectedMethodType == RechargeMethodType.nativeStore) { _processNativePurchase(); return; } _handleMiFaPayPrimaryAction(miFaPayManager); }, ), SizedBox(height: 14.w), ], ), ), ), ], ), ), ), ], ); } List _buildMethodOptions() { return [ RechargeMethodOption( type: RechargeMethodType.nativeStore, title: Platform.isAndroid ? 'Google Pay' : 'Apple Pay', assetIconPath: Platform.isAndroid ? 'sc_images/login/sc_icon_google.png' : null, iconData: Platform.isIOS ? Icons.apple : null, ), const RechargeMethodOption( type: RechargeMethodType.miFaPay, title: 'MiFaPay', assetIconPath: 'sc_images/index/sc_icon_recharge_agency.png', ), ]; } void _showRecordDialog() { showGeneralDialog( context: context, barrierLabel: '', barrierDismissible: true, transitionDuration: const Duration(milliseconds: 350), barrierColor: SCGlobalConfig.businessLogicStrategy .getRechargePageDialogBarrierColor(), pageBuilder: ( BuildContext context, Animation animation, Animation secondaryAnimation, ) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { Navigator.pop(context); setState(() {}); }, child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ SizedBox(height: height(68)), Row( children: [ const Spacer(), GestureDetector( child: Container( decoration: BoxDecoration( color: SCGlobalConfig.businessLogicStrategy .getRechargePageDialogContainerBackgroundColor(), borderRadius: BorderRadius.circular(4), ), padding: EdgeInsets.symmetric( horizontal: 8.w, vertical: 3.w, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 5.w), GestureDetector( child: text( SCAppLocalizations.of(context)!.goldListort, textColor: SCGlobalConfig.businessLogicStrategy .getRechargePageDialogTextColor(), fontSize: 14.sp, ), onTap: () { Navigator.of(context).pop(); SCNavigatorUtils.push( context, WalletRoute.goldRecord, replace: false, ); }, ), SizedBox(height: 5.w), ], ), ), onTap: () {}, ), SizedBox(width: 10.w), ], ), Expanded(child: SizedBox(width: width(1))), ], ), ); }, ); } Widget _buildWalletSection() { return Container( margin: EdgeInsets.symmetric(horizontal: 15.w), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox(height: 45.w), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( SCGlobalConfig.businessLogicStrategy.getRechargePageGoldIcon(), width: 36.w, height: 36.w, ), SizedBox(width: 3.w), Consumer( builder: (context, ref, child) { return text( ref.myBalance > 1000 ? "${(ref.myBalance / 1000).toStringAsFixed(2)}k" : "${ref.myBalance}", fontSize: 16.sp, textColor: SCGlobalConfig.businessLogicStrategy .getRechargePageWalletTextColor(), fontWeight: FontWeight.bold, ); }, ), ], ), ], ), ); } Widget _buildMethodSection(MiFaPayManager miFaPayManager) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Recharge methods', style: TextStyle( fontSize: 15.sp, color: _accentGoldColor, fontWeight: FontWeight.w700, ), ), SizedBox(height: 10.w), ..._methodOptions.map( (RechargeMethodOption option) => _buildMethodTile(option, miFaPayManager), ), ], ); } Widget _buildMethodTile( RechargeMethodOption option, MiFaPayManager miFaPayManager, ) { final bool isSelected = _selectedMethodType == option.type; String trailingText = ''; if (option.type == RechargeMethodType.miFaPay && miFaPayManager.selectedCommodity != null) { trailingText = _formatMiFaPriceLabel(miFaPayManager.selectedCommodity!); } return Padding( padding: EdgeInsets.only(bottom: 10.w), child: SCDebounceWidget( onTap: () { setState(() { _selectedMethodType = option.type; }); if (option.type == RechargeMethodType.miFaPay) { _openMiFaPaySheet(miFaPayManager); } }, child: AnimatedContainer( duration: const Duration(milliseconds: 180), width: double.infinity, height: 58.w, padding: EdgeInsets.symmetric(horizontal: 14.w), decoration: BoxDecoration( gradient: LinearGradient( colors: isSelected ? const [Color(0x42FFFFFF), Color(0x24FFFFFF)] : const [Color(0x33FFFFFF), Color(0x18FFFFFF)], begin: AlignmentDirectional.centerStart, end: AlignmentDirectional.centerEnd, ), borderRadius: BorderRadius.circular(10.w), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), blurRadius: 10.w, offset: Offset(0, 3.w), ), ], ), child: Row( children: [ _buildMethodLeading(option), SizedBox(width: 14.w), Expanded( child: Text( option.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 15.sp, color: _surfaceTextColor, fontWeight: FontWeight.w600, ), ), ), if (trailingText.isNotEmpty) Padding( padding: EdgeInsetsDirectional.only(end: 8.w), child: Text( trailingText, style: TextStyle( fontSize: 12.sp, color: _surfaceSubtleTextColor.withValues(alpha: 0.82), fontWeight: FontWeight.w600, ), ), ), Icon( isSelected ? Icons.radio_button_checked : Icons.radio_button_off_outlined, size: 18.w, color: isSelected ? _accentGoldColor : _surfaceSubtleTextColor.withValues(alpha: 0.45), ), ], ), ), ), ); } Widget _buildMethodLeading(RechargeMethodOption option) { if (option.assetIconPath != null) { return Image.asset( option.assetIconPath!, width: 28.w, height: 28.w, fit: BoxFit.contain, ); } return Icon( option.iconData ?? CupertinoIcons.creditcard, size: 24.w, color: _surfaceTextColor, ); } Widget _buildNativeProductList() { return Platform.isAndroid ? Consumer( builder: (context, ref, child) { return ref.products.isNotEmpty ? ListView.separated( itemCount: ref.products.length, padding: EdgeInsets.zero, itemBuilder: (context, index) { return _buildGoogleProductItem( ref.products[index], ref, index, ); }, separatorBuilder: (BuildContext context, int index) { return SizedBox(height: 10.w); }, ) : _buildRechargeEmptyState(); }, ) : Consumer( builder: (context, ref, child) { return ref.products.isNotEmpty ? ListView.separated( itemCount: ref.products.length, padding: EdgeInsets.zero, itemBuilder: (context, index) { return _buildAppleProductItem( ref.products[index], ref, index, ); }, separatorBuilder: (BuildContext context, int index) { return SizedBox(height: 10.w); }, ) : _buildRechargeEmptyState(); }, ); } Widget _buildMiFaPayContent(MiFaPayManager miFaPayManager) { if (miFaPayManager.isLoading && miFaPayManager.commodities.isEmpty) { return Center(child: CupertinoActivityIndicator(radius: 14.w)); } if (miFaPayManager.errorMessage.isNotEmpty && miFaPayManager.commodities.isEmpty) { return _buildMiFaPayStatusCard( title: 'MiFaPay is temporarily unavailable', detail: miFaPayManager.errorMessage, actionText: 'Retry', onTap: () { miFaPayManager.reload(); }, ); } if (miFaPayManager.commodities.isEmpty) { return _buildMiFaPayStatusCard( title: 'No MiFaPay products available', detail: 'Please try again later.', ); } final SCMiFaPayCommodityItemRes? commodity = miFaPayManager.selectedCommodity; final SCMiFaPayChannelRes? channel = miFaPayManager.selectedChannel; final SCMiFaPayCountryRes? country = miFaPayManager.selectedCountry; return _buildMiFaPayStatusCard( title: commodity == null ? 'Tap MiFaPay to choose an amount' : 'Selected ${_formatMiFaPriceLabel(commodity)}', detail: commodity == null ? 'Choose amount and payment channel in the bottom sheet.' : '${_formatWholeNumber(_parseWholeNumber(commodity.content))} +${_formatWholeNumber(_parseWholeNumber(commodity.awardContent))}', actionText: channel == null ? null : '${channel.channelName ?? ''}${country?.alphaTwo?.isNotEmpty == true ? ' ${country!.alphaTwo}' : ''}', ); } Widget _buildMiFaPayStatusCard({ required String title, required String detail, String? actionText, VoidCallback? onTap, }) { return Center( child: Container( width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 16.w), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0x33FFFFFF), Color(0x18FFFFFF)], begin: AlignmentDirectional.topStart, end: AlignmentDirectional.bottomEnd, ), borderRadius: BorderRadius.circular(10.w), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), blurRadius: 10.w, offset: Offset(0, 3.w), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( title, textAlign: TextAlign.center, style: TextStyle( fontSize: 14.sp, color: _surfaceTextColor, fontWeight: FontWeight.w700, ), ), SizedBox(height: 8.w), Text( detail, textAlign: TextAlign.center, style: TextStyle( fontSize: 12.sp, color: _surfaceSubtleTextColor, fontWeight: FontWeight.w500, ), ), if ((actionText ?? '').isNotEmpty) ...[ SizedBox(height: 10.w), GestureDetector( onTap: onTap, child: Text( actionText!, textAlign: TextAlign.center, style: TextStyle( fontSize: 12.sp, color: onTap != null ? _accentGoldColor : _surfaceSubtleTextColor.withValues(alpha: 0.82), fontWeight: FontWeight.w700, ), ), ), ], ], ), ), ); } Widget _buildRechargeEmptyState() { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Image.asset( 'sc_images/general/sc_icon_loading.png', width: 96.w, fit: BoxFit.fitWidth, color: Colors.white.withValues(alpha: 0.72), ), SizedBox(height: 10.w), Text( SCAppLocalizations.of(context)!.noData, style: TextStyle( fontSize: 13.sp, color: _accentGoldColor.withValues(alpha: 0.9), fontWeight: FontWeight.w500, ), ), ], ), ); } Widget _buildFooterButton({ required String title, required VoidCallback onTap, }) { return SCDebounceWidget( onTap: onTap, child: Container( alignment: Alignment.center, margin: EdgeInsets.symmetric(horizontal: 60.w), height: 42.w, decoration: BoxDecoration( color: SCGlobalConfig.businessLogicStrategy .getRechargePageButtonBackgroundColor(), borderRadius: BorderRadius.all(Radius.circular(height(8))), ), child: text( title, textColor: SCGlobalConfig.businessLogicStrategy .getRechargePageButtonTextColor(), fontSize: 16.sp, ), ), ); } void _processNativePurchase() { if (Platform.isAndroid) { Provider.of( context, listen: false, ).processPurchase(); } else if (Platform.isIOS) { Provider.of( context, listen: false, ).processPurchase(); } } String _buildMiFaPayPrimaryTitle(MiFaPayManager miFaPayManager) { if (miFaPayManager.isCreatingOrder) { return 'Processing...'; } if (miFaPayManager.isLoading && miFaPayManager.commodities.isEmpty) { return 'Loading...'; } if (miFaPayManager.errorMessage.isNotEmpty && miFaPayManager.commodities.isEmpty) { return 'Retry'; } return miFaPayManager.canCreateRecharge ? 'Pay now' : 'Choose amount'; } void _handleMiFaPayPrimaryAction(MiFaPayManager miFaPayManager) { if (miFaPayManager.isCreatingOrder) { return; } if (miFaPayManager.errorMessage.isNotEmpty && miFaPayManager.commodities.isEmpty) { miFaPayManager.reload(); return; } if (miFaPayManager.canCreateRecharge) { miFaPayManager.createRecharge(context); return; } _openMiFaPaySheet(miFaPayManager); } void _openMiFaPaySheet(MiFaPayManager miFaPayManager) { if (miFaPayManager.isLoading && miFaPayManager.commodities.isEmpty) { SCTts.show('MiFaPay products are loading'); return; } final List packages = _buildMiFaPackages( miFaPayManager.commodities, ); final List channels = _buildMiFaChannels( miFaPayManager.channels, ); if (packages.isEmpty || channels.isEmpty) { if (miFaPayManager.errorMessage.isNotEmpty) { SCTts.show(miFaPayManager.errorMessage); } else { SCTts.show('No MiFaPay product is available now'); } return; } showBottomInBottomDialog( context, RechargeMethodBottomSheet( method: _methodOptions.firstWhere( (RechargeMethodOption item) => item.type == RechargeMethodType.miFaPay, ), packages: packages, channels: channels, initialPackageId: miFaPayManager.selectedCommodity?.id, initialChannelCode: miFaPayManager.selectedChannel?.channelCode, onConfirm: ( RechargePackageOption package, RechargeChannelOption channel, ) { setState(() { _selectedMethodType = RechargeMethodType.miFaPay; }); miFaPayManager.chooseCommodityById(package.id); miFaPayManager.chooseChannelByCode(channel.code); }, ), barrierColor: Colors.black.withValues(alpha: 0.35), ); } List _buildMiFaPackages( List commodities, ) { return commodities .where((SCMiFaPayCommodityItemRes item) => (item.id ?? '').isNotEmpty) .map((SCMiFaPayCommodityItemRes item) { final int coins = _parseWholeNumber(item.content); final int bonusCoins = _parseWholeNumber(item.awardContent); return RechargePackageOption( id: item.id ?? '', coins: coins, bonusCoins: bonusCoins, priceLabel: _formatMiFaPriceLabel(item), badge: _buildMiFaBadge(coins, bonusCoins), ); }) .toList(growable: false); } List _buildMiFaChannels( List channels, ) { return channels .where( (SCMiFaPayChannelRes item) => (item.channelCode ?? '').isNotEmpty, ) .map((SCMiFaPayChannelRes item) { return RechargeChannelOption( code: item.channelCode ?? '', name: item.channelName ?? '', typeLabel: _buildChannelTypeLabel(item), ); }) .toList(growable: false); } String _buildMiFaBadge(int coins, int bonusCoins) { if (coins <= 0 || bonusCoins <= 0) { return ''; } final double percent = bonusCoins * 100 / coins; return '+${percent.toStringAsFixed(percent >= 10 ? 0 : 1)}%'; } String _buildChannelTypeLabel(SCMiFaPayChannelRes channel) { final List parts = []; if ((channel.channelType ?? '').isNotEmpty) { parts.add(channel.channelType!); } if ((channel.factoryChannel ?? '').isNotEmpty) { parts.add(channel.factoryChannel!); } return parts.join(' '); } String _formatMiFaPriceLabel(SCMiFaPayCommodityItemRes item) { final String currency = item.currency ?? 'USD'; final num amount = item.amount ?? item.amountUsd ?? 0; final String formattedAmount = amount % 1 == 0 ? amount.toInt().toString() : amount.toString(); return '$currency $formattedAmount'; } int _parseWholeNumber(String? value) { if ((value ?? '').isEmpty) { return 0; } return num.tryParse(value!)?.toInt() ?? 0; } String _formatWholeNumber(num value) { final String raw = value.toInt().toString(); final StringBuffer buffer = StringBuffer(); for (int i = 0; i < raw.length; i++) { final int position = raw.length - i; buffer.write(raw[i]); if (position > 1 && position % 3 == 1) { buffer.write(','); } } return buffer.toString(); } Widget _buildGoogleProductItem( SelecteProductConfig productConfig, AndroidPaymentProcessor ref, int index, ) { return GestureDetector( child: Container( padding: EdgeInsets.symmetric(vertical: 5.w, horizontal: 10.w), decoration: BoxDecoration( borderRadius: BorderRadius.circular(10.w), gradient: LinearGradient( colors: productConfig.isSelecte ? const [Color(0x42FFFFFF), Color(0x24FFFFFF)] : const [Color(0x33FFFFFF), Color(0x18FFFFFF)], begin: AlignmentDirectional.centerStart, end: AlignmentDirectional.centerEnd, ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), blurRadius: 10.w, offset: Offset(0, 3.w), ), ], ), child: Row( children: [ Image.asset( SCGlobalConfig.businessLogicStrategy .getRechargePageGoldIconByIndex(index), width: 34.w, height: 34.w, ), SizedBox(width: 5.w), text( "${ref.productMap[productConfig.produc.id]?.obtainCandy ?? 0}", fontSize: 12.sp, textColor: _surfaceTextColor, ), const Spacer(), text( productConfig.produc.price, textColor: _surfaceSubtleTextColor, fontWeight: FontWeight.w600, fontSize: 12.sp, ), ], ), ), onTap: () { ref.chooseProductConfig(index); }, ); } Widget _buildAppleProductItem( SelecteProductConfig productConfig, IOSPaymentProcessor ref, int index, ) { return GestureDetector( child: Container( padding: EdgeInsets.symmetric(vertical: 5.w, horizontal: 10.w), decoration: BoxDecoration( borderRadius: BorderRadius.circular(10.w), gradient: LinearGradient( colors: productConfig.isSelecte ? const [Color(0x42FFFFFF), Color(0x24FFFFFF)] : const [Color(0x33FFFFFF), Color(0x18FFFFFF)], begin: AlignmentDirectional.centerStart, end: AlignmentDirectional.centerEnd, ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), blurRadius: 10.w, offset: Offset(0, 3.w), ), ], ), child: Row( children: [ Image.asset( SCGlobalConfig.businessLogicStrategy .getRechargePageGoldIconByIndex(index), width: 34.w, height: 34.w, ), SizedBox(width: 5.w), text( "${ref.productMap[productConfig.produc.id]?.obtainCandy ?? 0}", fontSize: 12.sp, textColor: _surfaceTextColor, ), const Spacer(), text( productConfig.produc.price, textColor: _surfaceSubtleTextColor, fontWeight: FontWeight.w600, fontSize: 12.sp, ), ], ), ), onTap: () { ref.chooseProductConfig(index); }, ); } } class SelecteProductConfig { ProductDetails produc; bool isSelecte = false; SelecteProductConfig(this.produc, this.isSelecte); }