import 'dart:async'; import 'package:decimal/decimal.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../core/network/dio_client.dart'; import '../core/utils/dialog_utils.dart' show extractErrorMessage; import '../data/models/asset/account_auth.dart'; import '../data/models/asset/deposit_wallet.dart'; import '../data/models/asset/withdraw_balance.dart'; import '../data/models/asset/withdraw_record.dart'; import '../data/services/asset_service.dart'; import '../data/services/withdraw_service.dart'; // ══════════════════════════════════════════════════════════════ // Withdraw State // ══════════════════════════════════════════════════════════════ class WithdrawState { /// 0=链上提币, 1=内部转账 final int tabIndex; /// 钱包列表(含网络信息,复用充币的 DepositWallet) final List usdtWallets; /// 当前选中的网络索引 final int selectedNetworkIndex; /// 可用余额 final WithdrawBalance? balance; /// 认证信息 final AccountAuth? auth; /// 验证码倒计时秒数(0=可发送) final int codeCountdown; /// 提交中 final bool isSubmitting; final bool isLoading; final String? errorMessage; /// 内部转账最小金额(USDT 基础配置,null 表示未加载) final Decimal? _transferMinAmount; const WithdrawState({ this.tabIndex = 0, this.usdtWallets = const [], this.selectedNetworkIndex = -1, this.balance, this.auth, Decimal? transferMinAmount, this.codeCountdown = 0, this.isSubmitting = false, this.isLoading = false, this.errorMessage, }) : _transferMinAmount = transferMinAmount; /// 当前选中的钱包(链上提币模式下使用) DepositWallet? get selectedWallet => selectedNetworkIndex >= 0 && usdtWallets.isNotEmpty && selectedNetworkIndex < usdtWallets.length ? usdtWallets[selectedNetworkIndex] : null; /// 当前币种信息 WalletCoin? get currentCoin => selectedWallet?.coin; /// 网络名称列表 List get networkNames => usdtWallets.map((w) => w.coin?.networkName ?? '').toList(); /// 链上提币的 unit(币种代号) String get onChainUnit => currentCoin?.code ?? 'TUSDT'; /// 手续费(withdrawFeeValue) Decimal get fee => currentCoin?.fee ?? Decimal.zero; /// 链上提币最小额 Decimal get minWithdrawAmount => currentCoin?.minWithdrawAmount ?? Decimal.zero; /// 内部转账最小额(USDT 基础配置) Decimal get transferMinAmount => _transferMinAmount ?? Decimal.zero; /// 当前模式下的最小额(链上 or 内部转账) Decimal get currentMinAmount => tabIndex == 0 ? minWithdrawAmount : transferMinAmount; /// 可提现余额(链上提币) Decimal get withdrawableBalance => balance?.withdrawableBalance ?? Decimal.zero; /// 可转账余额(内部转账) Decimal get transferableBalance => balance?.transferableBalance ?? Decimal.zero; /// 当前可用余额(根据 Tab 决定) Decimal get availableBalance => tabIndex == 0 ? withdrawableBalance : transferableBalance; /// 是否已绑定 Google 验证 bool get isGoogleBound => auth?.isGoogleVerified ?? false; WithdrawState copyWith({ int? tabIndex, List? usdtWallets, int? selectedNetworkIndex, WithdrawBalance? balance, AccountAuth? auth, Decimal? transferMinAmount, int? codeCountdown, bool? isSubmitting, bool? isLoading, String? errorMessage, }) => WithdrawState( tabIndex: tabIndex ?? this.tabIndex, usdtWallets: usdtWallets ?? this.usdtWallets, selectedNetworkIndex: selectedNetworkIndex ?? this.selectedNetworkIndex, balance: balance ?? this.balance, auth: auth ?? this.auth, transferMinAmount: transferMinAmount ?? _transferMinAmount, codeCountdown: codeCountdown ?? this.codeCountdown, isSubmitting: isSubmitting ?? this.isSubmitting, isLoading: isLoading ?? this.isLoading, errorMessage: errorMessage, ); } // ══════════════════════════════════════════════════════════════ // Withdraw Notifier // ══════════════════════════════════════════════════════════════ class WithdrawNotifier extends Notifier { Timer? _countdownTimer; @override WithdrawState build() { ref.onDispose(() => _countdownTimer?.cancel()); Future.microtask(_load); return const WithdrawState(isLoading: true); } Future _load() async { state = state.copyWith(isLoading: true, errorMessage: null, selectedNetworkIndex: -1); try { final dio = ref.read(dioClientProvider); final assetService = AssetService(dio); final withdrawService = WithdrawService(dio); // 并发请求:钱包地址 + 可用余额 + 认证信息 + 内部转账最小额 final results = await Future.wait([ assetService.getWalletAddresses(), withdrawService.getBalance('usdt'), withdrawService.getSecuritySetting(), withdrawService.getTransferMinAmount(), ]); final wallets = results[0] as List; final balance = results[1] as WithdrawBalance; final auth = results[2] as AccountAuth; final transferMinAmount = results[3] as Decimal; // 过滤 USDT 钱包,TRC20 排最前 const networkOrder = {'TRC20': 0, 'ERC20': 1, 'BEP20': 2}; final usdtWallets = wallets .where((w) => w.coin != null && w.coin!.code.contains('USDT')) .toList() ..sort((a, b) { final oa = networkOrder[a.coin!.networkName] ?? 99; final ob = networkOrder[b.coin!.networkName] ?? 99; return oa.compareTo(ob); }); state = state.copyWith( usdtWallets: usdtWallets, balance: balance, auth: auth, transferMinAmount: transferMinAmount, selectedNetworkIndex: -1, isLoading: false, ); } catch (e) { state = state.copyWith(isLoading: false, errorMessage: extractErrorMessage(e)); } } Future refresh() => _load(); void setTab(int index) { state = state.copyWith(tabIndex: index); } void selectNetwork(int index) { state = state.copyWith(selectedNetworkIndex: index); } /// 发送邮箱验证码,返回错误信息(null 表示成功) Future sendEmailCode({ required String address, required String amount, }) async { if (state.tabIndex == 0 && state.selectedNetworkIndex < 0) return 'errSelectNetwork'; if (address.isEmpty) return 'errEnterAddress'; if (amount.isEmpty) return 'errEnterAmount'; try { final dio = ref.read(dioClientProvider); await WithdrawService(dio).sendWithdrawEmailCode( unit: state.tabIndex == 1 ? 'USDT' : state.onChainUnit, address: address, amount: amount, ); // 启动 60 秒倒计时 state = state.copyWith(codeCountdown: 60); _countdownTimer?.cancel(); _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { final remaining = state.codeCountdown - 1; if (remaining <= 0) { timer.cancel(); state = state.copyWith(codeCountdown: 0); } else { state = state.copyWith(codeCountdown: remaining); } }); return null; } catch (e) { return extractErrorMessage(e); } } /// 链上提币提交 Future submitOnChainWithdraw({ required String address, required String amount, required String jyPassword, required String vcode, required String vcode2, }) async { state = state.copyWith(isSubmitting: true, errorMessage: null); try { final dio = ref.read(dioClientProvider); await WithdrawService(dio).withdrawApply( unit: state.onChainUnit, amount: amount, address: address, fee: state.fee.toString(), vcode: vcode, jyPassword: jyPassword, vcode2: vcode2, ); state = state.copyWith(isSubmitting: false); // 刷新余额 _load(); return true; } catch (e) { state = state.copyWith(isSubmitting: false, errorMessage: extractErrorMessage(e)); return false; } } /// 内部转账提交 Future submitInternalTransfer({ required String address, required String amount, required String jyPassword, required String vcode, required String vcode2, }) async { state = state.copyWith(isSubmitting: true, errorMessage: null); try { final dio = ref.read(dioClientProvider); await WithdrawService(dio).internalTransfer( unit: 'USDT', // 内部转账用币种名 amount: amount, address: address, vcode: vcode, jyPassword: jyPassword, vcode2: vcode2, ); state = state.copyWith(isSubmitting: false); _load(); return true; } catch (e) { state = state.copyWith(isSubmitting: false, errorMessage: extractErrorMessage(e)); return false; } } /// 表单校验 String? validate({ required String address, required String amount, required String jyPassword, required String vcode, required String vcode2, }) { if (state.tabIndex == 0 && state.selectedNetworkIndex < 0) return 'errSelectNetwork'; if (address.isEmpty) return 'errEnterAddress'; if (amount.isEmpty) return 'errEnterAmount'; if (jyPassword.isEmpty) return 'errEnterFundPassword'; if (vcode.isEmpty) return 'errEnterVerifyCode'; if (!state.isGoogleBound) return 'errBindGoogleFirst'; if (vcode2.isEmpty) return 'errEnterGoogleCode'; final amountDecimal = Decimal.tryParse(amount); if (amountDecimal == null) return 'errAmountFormat'; if (amountDecimal < state.currentMinAmount) { return state.tabIndex == 0 ? 'errMinWithdraw:${state.currentMinAmount}' : 'errMinTransfer:${state.currentMinAmount}'; } if (amountDecimal > state.availableBalance) { return 'errExceedBalance'; } return null; } } final withdrawProvider = NotifierProvider( WithdrawNotifier.new, ); // ══════════════════════════════════════════════════════════════ // Withdraw History // ══════════════════════════════════════════════════════════════ class WithdrawHistoryState { final List records; final bool isLoading; final bool hasMore; /// 链上提币当前页(后端 page 从 0 开始) final int withdrawPage; /// 内部转账当前页(后端 pageNo 从 0 开始) final int transferPage; final String? errorMessage; const WithdrawHistoryState({ this.records = const [], this.isLoading = false, this.hasMore = true, this.withdrawPage = 0, this.transferPage = 0, this.errorMessage, }); WithdrawHistoryState copyWith({ List? records, bool? isLoading, bool? hasMore, int? withdrawPage, int? transferPage, String? errorMessage, }) => WithdrawHistoryState( records: records ?? this.records, isLoading: isLoading ?? this.isLoading, hasMore: hasMore ?? this.hasMore, withdrawPage: withdrawPage ?? this.withdrawPage, transferPage: transferPage ?? this.transferPage, errorMessage: errorMessage, ); } class WithdrawHistoryNotifier extends Notifier { static const _pageSize = 10; @override WithdrawHistoryState build() { Future.microtask(_loadFirst); return const WithdrawHistoryState(isLoading: true); } Future _loadFirst() async { state = state.copyWith(isLoading: true, errorMessage: null); try { final service = WithdrawService(ref.read(dioClientProvider)); final results = await Future.wait([ service.getWithdrawRecords(page: 0, pageSize: _pageSize), service.getTransferRecords(pageNo: 0, pageSize: _pageSize), ]); final withdrawRecords = results[0] as List; final transferRecords = (results[1] as List) .map((r) => r.copyWith(isTransfer: true)) .toList(); final all = _merge(withdrawRecords, transferRecords); state = state.copyWith( records: all, isLoading: false, withdrawPage: 0, transferPage: 0, hasMore: withdrawRecords.length >= _pageSize || transferRecords.length >= _pageSize, ); } catch (e) { state = state.copyWith(isLoading: false, errorMessage: extractErrorMessage(e)); } } Future refresh() => _loadFirst(); Future loadMore() async { if (state.isLoading || !state.hasMore) return; state = state.copyWith(isLoading: true); try { final service = WithdrawService(ref.read(dioClientProvider)); final nextW = state.withdrawPage + 1; final nextT = state.transferPage + 1; final results = await Future.wait([ service.getWithdrawRecords(page: nextW, pageSize: _pageSize), service.getTransferRecords(pageNo: nextT, pageSize: _pageSize), ]); final withdrawRecords = results[0] as List; final transferRecords = (results[1] as List) .map((r) => r.copyWith(isTransfer: true)) .toList(); final appended = _merge(withdrawRecords, transferRecords); state = state.copyWith( records: [...state.records, ...appended], isLoading: false, withdrawPage: nextW, transferPage: nextT, hasMore: withdrawRecords.length >= _pageSize || transferRecords.length >= _pageSize, ); } catch (e) { state = state.copyWith(isLoading: false, errorMessage: extractErrorMessage(e)); } } /// 合并两类记录,按 createTime 倒序 List _merge( List withdrawRecords, List transferRecords, ) { final all = [...withdrawRecords, ...transferRecords]; all.sort((a, b) => b.createTime.compareTo(a.createTime)); return all; } /// 取消链上提现 Future cancelWithdraw(String id) async { try { final dio = ref.read(dioClientProvider); await WithdrawService(dio).cancelWithdraw(id); await refresh(); return true; } catch (e) { state = state.copyWith(errorMessage: extractErrorMessage(e)); return false; } } } final withdrawHistoryProvider = NotifierProvider( WithdrawHistoryNotifier.new, );