import 'dart:io'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gal/gal.dart'; import 'package:go_router/go_router.dart'; import 'package:path_provider/path_provider.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:reown_appkit/modal/services/coinbase_service/utils/coinbase_utils.dart'; import 'package:reown_appkit/reown_appkit.dart'; import '../../../core/config/app_config.dart'; import '../../../core/l10n/app_localizations.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/utils/top_toast.dart'; import '../../../core/wallet/evm_recharge.dart'; import '../../../core/wallet/tron_recharge.dart'; import '../../../core/wallet/wallet_connect_recharge_helper.dart'; import '../../../core/wallet/wallet_connect_tron_recharge.dart'; import '../../../data/models/asset/recharge_order.dart'; import '../../../providers/deposit_provider.dart'; /// AppKit 默认只为 eip155/solana 填 methods;`tron` 会为 `[]`,钱包不会授权 `tron_signTransaction`。 Map _depositAppKitOptionalNamespaces() { final map = {}; for (final ns in ReownAppKitModalNetworks.getAllSupportedNamespaces()) { final networks = ReownAppKitModalNetworks.getAllSupportedNetworks(namespace: ns); final methods = ns == 'tron' ? ['tron_signTransaction', 'tron_signMessage'] : (NetworkUtils.defaultNetworkMethods[ns] ?? []); map[ns] = RequiredNamespace( chains: networks.map((e) => e.chainId).toList(), methods: methods, events: NetworkUtils.defaultNetworkEvents[ns] ?? [], ); } return map; } /// 充币:与 Web `DepositView.vue` + `useWalletConnectDeposit` 对齐(订单 + WalletConnect / 手动 Hash)。 class DepositScreen extends ConsumerStatefulWidget { const DepositScreen({super.key}); @override ConsumerState createState() => _DepositScreenState(); } class _DepositScreenState extends ConsumerState with SingleTickerProviderStateMixin { static const int _tabManual = 0; static const int _tabOnChain = 1; late final TabController _depositModeTab; final _qrKey = GlobalKey(); final _amountCtrl = TextEditingController(); final _hashCtrl = TextEditingController(); bool _savingQr = false; ReownAppKitModal? _appKitModal; bool _appKitInitializing = false; @override void initState() { super.initState(); _depositModeTab = TabController(length: 2, vsync: this); } @override void dispose() { _depositModeTab.dispose(); _amountCtrl.dispose(); _hashCtrl.dispose(); _appKitModal?.disconnect(); _appKitModal?.dispose(); super.dispose(); } Future _ensureAppKitModal() async { final projectId = AppConfig.walletConnectProjectId; if (projectId.isEmpty) { return; } if (_appKitModal != null) { return; } if (_appKitInitializing) { return; } _appKitInitializing = true; try { ReownAppKitModalNetworks.removeSupportedNetworks('solana'); ReownAppKitModalNetworks.addSupportedNetworks('tron', [ ReownAppKitModalNetworkInfo( name: 'Tron', chainId: kTronMainnetChainIdHex, chainIcon: 'https://pbs.twimg.com/profile_images/1761904730668675072/v98T7vRL_400x400.jpg', currency: 'TRX', rpcUrl: AppConfig.tronFullHost, explorerUrl: 'https://tronscan.org', ), ]); final m = ReownAppKitModal( context: context, projectId: projectId, logLevel: kDebugMode ? LogLevel.error : LogLevel.nothing, disconnectOnDispose: true, /// 跳过 Coinbase Wallet 的 Explorer 单钱包拉取,避免弱网环境下访问 api.web3modal.com 被 RST 时阻塞/报错(充币不需 Coinbase)。 excludedWalletIds: {CoinbaseUtils.walletId}, metadata: PairingMetadata( name: 'iBit', description: AppLocalizations.of(context)!.walletConnectPairingDescription, url: 'https://ibit123.com', icons: const [ 'https://raw.githubusercontent.com/reown-com/reown_flutter/refs/heads/develop/assets/appkit_logo.png', ], redirect: Redirect( native: 'ibit://', universal: 'https://ibit123.com', linkMode: false, ), ), optionalNamespaces: _depositAppKitOptionalNamespaces(), featuresConfig: FeaturesConfig(socials: const []), ); await m.init(); if (mounted) { setState(() { _appKitModal = m; }); } } finally { _appKitInitializing = false; } } Future _saveQr(String address) async { if (_savingQr || address.isEmpty) { return; } setState(() => _savingQr = true); try { final hasAccess = await Gal.hasAccess(toAlbum: true); if (!hasAccess) { final granted = await Gal.requestAccess(toAlbum: true); if (!granted) { if (mounted) { showTopToast(context, message: AppLocalizations.of(context)!.saveFailed); } return; } } await WidgetsBinding.instance.endOfFrame; final boundary = _qrKey.currentContext!.findRenderObject() as RenderRepaintBoundary; final image = await boundary.toImage(pixelRatio: 3.0); final byteData = await image.toByteData(format: ui.ImageByteFormat.png); final bytes = byteData!.buffer.asUint8List(); final tempDir = await getTemporaryDirectory(); final file = File( '${tempDir.path}/deposit_qr_${DateTime.now().millisecondsSinceEpoch}.png'); await file.writeAsBytes(bytes); await Gal.putImage(file.path); if (mounted) { showTopToast(context, message: AppLocalizations.of(context)!.saveSuccess); } } catch (e) { if (mounted) { showTopToast(context, message: AppLocalizations.of(context)!.saveFailed); } } finally { if (mounted) { setState(() => _savingQr = false); } } } bool _validateAmount(AppLocalizations l10n) { final s = _amountCtrl.text.trim(); if (s.isEmpty) { showTopToast(context, message: l10n.depositEnterAmount); return false; } final n = num.tryParse(s.replaceAll(',', '')); if (n == null || n <= 0) { showTopToast(context, message: l10n.depositAmountPositive); return false; } return true; } Future _onCreateOrder() async { final l10n = AppLocalizations.of(context)!; if (!_validateAmount(l10n)) { return; } await ref.read(depositProvider.notifier).createRechargeOrder(_amountCtrl.text); final err = ref.read(depositProvider).errorMessage; if (!mounted) { return; } if (err != null) { showTopToast(context, message: err); } else { _hashCtrl.clear(); showTopToast(context, message: l10n.depositOrderCreated); } } Future _onSubmitHash() async { final l10n = AppLocalizations.of(context)!; final h = _hashCtrl.text.trim(); if (h.isEmpty) { showTopToast(context, message: l10n.depositTxHashPlaceholder); return; } await ref.read(depositProvider.notifier).submitTxHash(h); final err = ref.read(depositProvider).errorMessage; if (!mounted) { return; } if (err != null) { showTopToast(context, message: err); } else { showTopToast(context, message: l10n.confirm); } } Future _onWalletPay( RechargeFlatNetworkOption net, RechargeOrderDetail order, ) async { final l10n = AppLocalizations.of(context)!; if (AppConfig.walletConnectProjectId.isEmpty) { showTopToast(context, message: l10n.depositWalletConnectNotConfigured); return; } ref.read(depositProvider.notifier).setWalletPayBusy(true); try { await _ensureAppKitModal(); final modal = _appKitModal; if (modal == null) { return; } final hash = _isTronLike(net) ? await WalletConnectTronRecharge.connectAndPayTron( appKitModal: modal, network: net, order: order, ) : await WalletConnectRechargeHelper.connectAndPay( appKitModal: modal, network: net, order: order, ); if (!mounted) { return; } _hashCtrl.text = hash; await ref.read(depositProvider.notifier).submitTxHash(hash); final err = ref.read(depositProvider).errorMessage; if (!mounted) { return; } if (err != null) { showTopToast(context, message: err); } else { showTopToast(context, message: l10n.confirm); } } catch (e) { if (mounted) { showTopToast(context, message: e.toString()); } } finally { ref.read(depositProvider.notifier).setWalletPayBusy(false); } } String _statusLabel(AppLocalizations l10n, int status) { switch (status) { case 0: return l10n.rechargeStatus0; case 1: return l10n.rechargeStatus1; case 2: return l10n.rechargeStatus2; case 3: return l10n.rechargeStatus3; case 4: return l10n.rechargeStatus4; default: return '$status'; } } bool _canWalletConnect( RechargeFlatNetworkOption? net, RechargeOrderDetail? order, ) { if (net == null || order == null || !order.isPendingPayment) { return false; } if (resolveEvmChainId(net.protocol, net.networkName) != null) { return true; } return isTronDepositNetwork(net.protocol, net.networkName); } bool _isTronLike(RechargeFlatNetworkOption? net) { if (net == null) { return false; } if (resolveEvmChainId(net.protocol, net.networkName) != null) { return false; } return isTronDepositNetwork(net.protocol, net.networkName); } String _networkProtocolLabel(RechargeFlatNetworkOption net) { final protocol = net.protocol.trim(); final name = net.networkName.trim(); if (protocol.isEmpty) { return name; } if (name.isEmpty) { return protocol; } return '$protocol·$name'; } String _subCoinLabel(RechargeFlatNetworkOption net) { final cn = net.coinNameCn.trim(); if (cn.isNotEmpty && cn != net.coinName) { return '${net.coinName} ($cn)'; } return net.coinName; } String _walletPayButtonLabel( AppLocalizations l10n, RechargeFlatNetworkOption? net, ) { if (net != null && resolveEvmChainId(net.protocol, net.networkName) != null) { return '${l10n.depositWalletConnectPay}(EVM)'; } return l10n.depositWalletConnectPay; } BoxDecoration _panelDecoration(ColorScheme cs, bool isDark) { return BoxDecoration( color: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary, borderRadius: BorderRadius.circular(12), border: Border.all(color: cs.outline.withAlpha(40)), ); } Widget _buildModeTabSwitcher( ColorScheme cs, AppLocalizations l10n, bool isDark, ) { return Container( height: 44, padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary, borderRadius: BorderRadius.circular(10), border: Border.all(color: cs.outline.withAlpha(50)), ), child: TabBar( controller: _depositModeTab, indicator: BoxDecoration( color: AppColors.brand, borderRadius: BorderRadius.circular(8), ), indicatorSize: TabBarIndicatorSize.tab, dividerColor: Colors.transparent, labelColor: Colors.black, unselectedLabelColor: cs.onSurface.withAlpha(180), labelStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), unselectedLabelStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), tabs: [ Tab(text: l10n.depositTabManual), Tab(text: l10n.depositTabOnChain), ], ), ); } void _resetToNewRecharge(DepositNotifier notifier) { notifier.clearCurrentOrder(); _amountCtrl.clear(); _hashCtrl.clear(); } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; final state = ref.watch(depositProvider); final notifier = ref.read(depositProvider.notifier); final isDark = Theme.of(context).brightness == Brightness.dark; final wcConfigured = AppConfig.walletConnectProjectId.isNotEmpty; final hasOrder = state.currentOrder != null; return ReownAppKitModalTheme( isDarkMode: isDark, child: Scaffold( appBar: AppBar( leading: IconButton( icon: const Icon(Icons.chevron_left, size: 28), onPressed: () => context.pop(), ), title: Text( hasOrder ? l10n.depositOrderInfo : l10n.depositCoin, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600), ), centerTitle: true, actions: [ TextButton( onPressed: hasOrder ? () => _resetToNewRecharge(notifier) : () => context.push('/asset/deposit/history'), child: Text( hasOrder ? l10n.depositNewRecharge : l10n.depositRecord, style: TextStyle( color: hasOrder ? AppColors.brand : cs.onSurface, fontSize: 14, fontWeight: FontWeight.w500, ), ), ), ], ), body: _buildBody(context, state, notifier, cs, l10n, isDark, wcConfigured: wcConfigured), ), ); } Widget _buildBody( BuildContext context, DepositState state, DepositNotifier notifier, ColorScheme cs, AppLocalizations l10n, bool isDark, { required bool wcConfigured, }) { if (state.loadingParents) { return const Center(child: CircularProgressIndicator()); } if (state.errorMessage != null && state.parentCoins.isEmpty && !state.loadingParents) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(state.errorMessage!, textAlign: TextAlign.center), const SizedBox(height: 16), FilledButton( onPressed: () => notifier.refresh(), child: Text(l10n.retry), ), ], ), ), ); } final order = state.currentOrder; return SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), child: order == null ? _buildOrderForm(context, state, notifier, cs, l10n, isDark) : _buildOrderDetail( context, state, notifier, cs, l10n, isDark, order, wcConfigured: wcConfigured, ), ); } Widget _buildOrderForm( BuildContext context, DepositState state, DepositNotifier notifier, ColorScheme cs, AppLocalizations l10n, bool isDark, ) { final net = state.selectedNetwork; final flatIndex = state.flatNetworks.isEmpty ? null : state.selectedFlatIndex.clamp(0, state.flatNetworks.length - 1); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildModeTabSwitcher(cs, l10n, isDark), const SizedBox(height: 20), Text( l10n.depositMainnetProtocol, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13), ), const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 12), decoration: _panelDecoration(cs, isDark), child: DropdownButtonHideUnderline( child: DropdownButton( isExpanded: true, value: flatIndex, hint: Text( state.childrenLoading ? l10n.loading : l10n.depositSelectNetworkFirst, style: TextStyle(color: cs.onSurface.withAlpha(140)), ), icon: Icon(Icons.keyboard_arrow_down, color: cs.onSurface.withAlpha(153)), items: [ for (var i = 0; i < state.flatNetworks.length; i++) DropdownMenuItem( value: i, child: Text( _networkProtocolLabel(state.flatNetworks[i]), style: TextStyle(color: cs.onSurface, fontSize: 15), ), ), ], onChanged: state.childrenLoading ? null : (v) { if (v != null) { notifier.selectFlatNetwork(v); } }, ), ), ), if (state.parentCoins.length > 1) ...[ const SizedBox(height: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 12), decoration: _panelDecoration(cs, isDark), child: DropdownButtonHideUnderline( child: DropdownButton( isExpanded: true, value: state.parentCoins.isEmpty ? null : state.selectedParentIndex.clamp( 0, state.parentCoins.length - 1), items: [ for (var i = 0; i < state.parentCoins.length; i++) DropdownMenuItem( value: i, child: Text( state.parentCoins[i].nameCn.isNotEmpty ? state.parentCoins[i].nameCn : state.parentCoins[i].name, style: TextStyle(color: cs.onSurface, fontSize: 15), ), ), ], onChanged: state.childrenLoading ? null : (v) { if (v != null) { notifier.selectParent(v); } }, ), ), ), ], if (state.childrenLoading) ...[ const SizedBox(height: 12), const LinearProgressIndicator(), ], if (net != null) ...[ const SizedBox(height: 12), Container( width: double.infinity, padding: const EdgeInsets.all(14), decoration: _panelDecoration(cs, isDark), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.depositSubCoin, style: TextStyle(color: cs.onSurface.withAlpha(140), fontSize: 12), ), const SizedBox(height: 4), Text( _subCoinLabel(net), style: TextStyle(color: cs.onSurface, fontSize: 14), ), if (net.hasTokenContract) ...[ const SizedBox(height: 12), Text( l10n.depositContractAddress, style: TextStyle( color: cs.onSurface.withAlpha(140), fontSize: 12), ), const SizedBox(height: 4), SelectableText( net.contractAddress, style: TextStyle( color: cs.onSurface, fontSize: 12, fontFamily: 'monospace', ), ), ], ], ), ), ], const SizedBox(height: 20), Text( l10n.depositEnterAmount, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13), ), const SizedBox(height: 8), TextField( controller: _amountCtrl, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: InputDecoration( filled: true, fillColor: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary, border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: cs.outline.withAlpha(50)), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: cs.outline.withAlpha(50)), ), hintText: l10n.depositEnterAmount, ), ), const SizedBox(height: 20), SizedBox( width: double.infinity, height: 48, child: FilledButton( style: FilledButton.styleFrom( backgroundColor: AppColors.brand, foregroundColor: Colors.black, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), onPressed: state.orderSubmitting || state.flatNetworks.isEmpty ? null : _onCreateOrder, child: state.orderSubmitting ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.black, ), ) : Text( l10n.depositCreateOrder, style: const TextStyle(fontWeight: FontWeight.w600), ), ), ), const SizedBox(height: 20), _buildRulesCard(context, cs, l10n, isDark, net?.coinName ?? ''), ], ); } Widget _buildRulesCard( BuildContext context, ColorScheme cs, AppLocalizations l10n, bool isDark, String currentCoin, ) { return Container( width: double.infinity, padding: const EdgeInsets.all(14), decoration: _panelDecoration(cs, isDark), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.depositRules, style: TextStyle( color: cs.onSurface.withAlpha(153), fontSize: 13, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), Text( l10n.depositRulesBody, style: TextStyle( color: cs.onSurface.withAlpha(140), fontSize: 12, height: 1.45, ), ), if (currentCoin.isNotEmpty) ...[ const SizedBox(height: 10), Text( l10n.depositCurrentCoin(currentCoin), style: TextStyle(color: cs.onSurface.withAlpha(120), fontSize: 12), ), ], ], ), ); } Widget _buildOrderDetail( BuildContext context, DepositState state, DepositNotifier notifier, ColorScheme cs, AppLocalizations l10n, bool isDark, RechargeOrderDetail order, { required bool wcConfigured, }) { final addr = order.rechargeAddress; final net = state.selectedNetwork; final canWc = _canWalletConnect(net, order); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: double.infinity, padding: const EdgeInsets.all(14), decoration: _panelDecoration(cs, isDark), child: Column( children: [ _orderMetaRow( context, l10n.depositOrderNo, order.orderNo, cs, copyable: true, ), _orderMetaRow( context, l10n.depositOrderStatus, _statusLabel(l10n, order.status), cs, valueColor: order.isPendingPayment ? AppColors.brand : null, ), _orderMetaRow(context, l10n.depositCurrency, order.coinName, cs), _orderMetaRow(context, l10n.depositNetwork, order.networkName, cs), _orderMetaRow( context, l10n.depositOrderAmount, order.amount, cs), ], ), ), if (order.isPendingPayment) ...[ const SizedBox(height: 16), _buildModeTabSwitcher(cs, l10n, isDark), const SizedBox(height: 16), AnimatedBuilder( animation: _depositModeTab, builder: (context, _) { if (_depositModeTab.index == _tabManual) { return _buildManualContent( context, state, cs, l10n, isDark, addr, net, order: order, ); } else if (_depositModeTab.index == _tabOnChain) { return _buildOnChainContent( context, state, cs, l10n, isDark, addr, net, canWc, wcConfigured: wcConfigured, order: order, ); } else { return const SizedBox.shrink(); } }, ), ] else ...[ const SizedBox(height: 16), _buildManualContent( context, state, cs, l10n, isDark, addr, net, order: order, readOnly: true, ), ], ], ); } Widget _orderMetaRow( BuildContext context, String label, String value, ColorScheme cs, { Color? valueColor, bool copyable = false, }) { final l10n = AppLocalizations.of(context)!; return Padding( padding: const EdgeInsets.only(bottom: 10), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 72, child: Text( label, style: TextStyle(color: cs.onSurface.withAlpha(140), fontSize: 13), ), ), Expanded( child: Text( value, style: TextStyle( color: valueColor ?? cs.onSurface, fontSize: 13, fontWeight: valueColor != null ? FontWeight.w600 : FontWeight.w400, ), ), ), if (copyable && value.isNotEmpty) TextButton( onPressed: () { Clipboard.setData(ClipboardData(text: value)); showTopToast(context, message: l10n.copied); }, style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 8), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: Text( l10n.depositCopy, style: const TextStyle( color: AppColors.brand, fontSize: 12, fontWeight: FontWeight.w600, ), ), ), ], ), ); } Widget _buildAddressBlock( BuildContext context, ColorScheme cs, AppLocalizations l10n, bool isDark, String addr, { bool showQr = false, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.depositReceivingAddress, style: TextStyle( color: cs.onSurface.withAlpha(153), fontSize: 13, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 6), Text( l10n.depositPayToHint, style: TextStyle(color: cs.onSurface.withAlpha(120), fontSize: 12), ), const SizedBox(height: 10), Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: _panelDecoration(cs, isDark), child: SelectableText( addr, style: TextStyle(color: cs.onSurface, fontSize: 13), ), ), if (showQr && addr.isNotEmpty) ...[ const SizedBox(height: 16), Text( l10n.depositQrReceive, style: TextStyle( color: cs.onSurface.withAlpha(153), fontSize: 13, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 10), Center( child: RepaintBoundary( key: _qrKey, child: Container( width: 200, height: 200, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), child: QrImageView( data: addr, version: QrVersions.auto, size: 180, backgroundColor: Colors.white, ), ), ), ), const SizedBox(height: 8), Text( l10n.depositQrHint, textAlign: TextAlign.center, style: TextStyle(color: cs.onSurface.withAlpha(120), fontSize: 12), ), const SizedBox(height: 8), Center( child: TextButton.icon( onPressed: addr.isNotEmpty && !_savingQr ? () => _saveQr(addr) : null, icon: _savingQr ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : Icon(Icons.download_outlined, size: 16, color: cs.onSurface.withAlpha(153)), label: Text( l10n.saveQrCode, style: TextStyle( color: cs.onSurface.withAlpha(153), fontSize: 13), ), ), ), ], ], ); } Widget _buildContractBlock( ColorScheme cs, AppLocalizations l10n, bool isDark, RechargeFlatNetworkOption? net, ) { if (net == null || !net.hasTokenContract) { return const SizedBox.shrink(); } return Padding( padding: const EdgeInsets.only(top: 12), child: Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: _panelDecoration(cs, isDark), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.depositContractAddress, style: TextStyle(color: cs.onSurface.withAlpha(140), fontSize: 12), ), const SizedBox(height: 6), SelectableText( net.contractAddress, style: TextStyle( color: cs.onSurface, fontSize: 12, fontFamily: 'monospace', ), ), ], ), ), ); } Widget _buildTxHashSection( BuildContext context, DepositState state, ColorScheme cs, AppLocalizations l10n, bool isDark, { bool enabled = true, }) { if (!enabled) { return const SizedBox.shrink(); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 20), Text( l10n.depositSubmitHashHint, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13), ), const SizedBox(height: 8), TextField( controller: _hashCtrl, decoration: InputDecoration( filled: true, fillColor: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary, hintText: l10n.depositTxHashPlaceholder, border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: cs.outline.withAlpha(50)), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: cs.outline.withAlpha(50)), ), ), ), const SizedBox(height: 12), SizedBox( width: double.infinity, height: 48, child: FilledButton( style: FilledButton.styleFrom( backgroundColor: AppColors.brand, foregroundColor: Colors.black, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), onPressed: state.hashSubmitting || state.walletPayBusy ? null : _onSubmitHash, child: state.hashSubmitting ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.black, ), ) : Text( l10n.depositSubmitHash, style: const TextStyle(fontWeight: FontWeight.w600), ), ), ), ], ); } Widget _buildOnChainContent( BuildContext context, DepositState state, ColorScheme cs, AppLocalizations l10n, bool isDark, String addr, RechargeFlatNetworkOption? net, bool canWc, { required bool wcConfigured, required RechargeOrderDetail order, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (addr.isNotEmpty) ...[ _buildAddressBlock(context, cs, l10n, isDark, addr), ], if (order.isPendingPayment) ...[ const SizedBox(height: 16), Container( width: double.infinity, padding: const EdgeInsets.all(14), decoration: _panelDecoration(cs, isDark), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (canWc && wcConfigured) Text( l10n.depositWalletConnectHint, style: TextStyle( color: cs.onSurface.withAlpha(153), fontSize: 12, height: 1.4, ), ) else if (canWc && !wcConfigured) Text( l10n.depositWalletConnectNotConfigured, style: TextStyle( color: cs.onSurface.withAlpha(120), fontSize: 12, height: 1.4, ), ) else if (_isTronLike(net)) Text( l10n.depositTronHint, style: TextStyle( color: cs.onSurface.withAlpha(153), fontSize: 12, height: 1.4, ), ), if (canWc && wcConfigured) ...[ const SizedBox(height: 12), SizedBox( width: double.infinity, height: 44, child: OutlinedButton( style: OutlinedButton.styleFrom( foregroundColor: AppColors.brand, side: const BorderSide(color: AppColors.brand), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), onPressed: (state.hashSubmitting || state.walletPayBusy || net == null) ? null : () => _onWalletPay(net, order), child: state.walletPayBusy ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, color: AppColors.brand, ), ) : Text(_walletPayButtonLabel(l10n, net)), ), ), ], ], ), ), _buildTxHashSection(context, state, cs, l10n, isDark), ] else if (order.txHash != null && order.txHash!.isNotEmpty) ...[ const SizedBox(height: 12), Text( '${l10n.depositTxHashPlaceholder}: ${order.txHash}', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 12), ), ], ], ); } Widget _buildManualContent( BuildContext context, DepositState state, ColorScheme cs, AppLocalizations l10n, bool isDark, String addr, RechargeFlatNetworkOption? net, { required RechargeOrderDetail order, bool readOnly = false, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (addr.isNotEmpty) ...[ _buildAddressBlock( context, cs, l10n, isDark, addr, showQr: true, ), ], _buildContractBlock(cs, l10n, isDark, net), if (order.isPendingPayment && !readOnly) _buildTxHashSection(context, state, cs, l10n, isDark) else if (order.txHash != null && order.txHash!.isNotEmpty) ...[ const SizedBox(height: 12), Text( '${l10n.depositTxHashPlaceholder}: ${order.txHash}', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 12), ), ], ], ); } }