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/withdraw_balance.dart'; import '../data/models/asset/withdraw_coin_option.dart'; import '../data/models/asset/withdraw_record.dart'; import '../data/services/withdraw_service.dart'; // ══════════════════════════════════════════════════════════════ // Withdraw State // ══════════════════════════════════════════════════════════════ class WithdrawState { /// 0=链上提币, 1=内部转账 final int tabIndex; final List networkList; final int selectedNetworkId; final List onchainSubCoins; final bool onchainSubCoinsLoading; final int selectedSubCoinIndex; final List transferMainCoins; final int selectedTransferCoinIndex; final List coinInfoList; final WithdrawBalance? balance; final AccountAuth? auth; final int codeCountdown; final bool isSubmitting; final bool isLoading; final String? errorMessage; const WithdrawState({ this.tabIndex = 0, this.networkList = const [], this.selectedNetworkId = 0, this.onchainSubCoins = const [], this.onchainSubCoinsLoading = false, this.selectedSubCoinIndex = 0, this.transferMainCoins = const [], this.selectedTransferCoinIndex = 0, this.coinInfoList = const [], this.balance, this.auth, this.codeCountdown = 0, this.isSubmitting = false, this.isLoading = false, this.errorMessage, }); WithdrawCoinOption? get onchainSelectedCoin => onchainSubCoins.isNotEmpty && selectedSubCoinIndex < onchainSubCoins.length ? onchainSubCoins[selectedSubCoinIndex] : null; WithdrawCoinOption? get transferSelectedCoin => transferMainCoins.isNotEmpty && selectedTransferCoinIndex < transferMainCoins.length ? transferMainCoins[selectedTransferCoinIndex] : null; Decimal get fee { final coin = onchainSelectedCoin; if (coin == null) { return Decimal.zero; } return Decimal.tryParse(coin.withdrawFee) ?? Decimal.zero; } Decimal get minWithdrawAmount { final coin = onchainSelectedCoin; if (coin == null) { return Decimal.zero; } final min = Decimal.tryParse(coin.minWithdraw) ?? Decimal.zero; if (min > Decimal.zero) { return min; } final defaults = networkWithdrawDefaults[coin.networkLabel]; if (defaults != null) { return Decimal.tryParse(defaults.min) ?? Decimal.zero; } return Decimal.zero; } Decimal get transferMinAmount { final coin = transferSelectedCoin; if (coin == null) { return Decimal.zero; } return Decimal.tryParse(coin.minTransfer) ?? Decimal.zero; } Decimal get currentMinAmount => tabIndex == 0 ? minWithdrawAmount : transferMinAmount; Decimal get withdrawableBalance => _balanceForCoin(onchainSelectedCoin, isTransfer: false); Decimal get transferableBalance => _balanceForCoin(transferSelectedCoin, isTransfer: true); Decimal get availableBalance => tabIndex == 0 ? withdrawableBalance : transferableBalance; String get onchainCoinSymbol => symbolForWithdrawCoin(onchainSelectedCoin); String get transferCoinSymbol => symbolForWithdrawCoin(transferSelectedCoin); String get onChainSubmitUnit => onchainSelectedCoin?.unit.toLowerCase() ?? 'usdt'; String get transferSubmitUnit => transferSelectedCoin?.unit.toLowerCase() ?? 'usdt'; bool get isGoogleBound => auth?.isGoogleVerified ?? false; Decimal _balanceForCoin(WithdrawCoinOption? coin, {required bool isTransfer}) { if (coin == null) { return Decimal.zero; } if (isTransfer && coin.unit.toUpperCase() == 'USDT') { return balance?.transferableBalance ?? balance?.balance ?? Decimal.zero; } if (!isTransfer && coin.isUsdtFamily) { return balance?.withdrawableBalance ?? balance?.balance ?? Decimal.zero; } return Decimal.tryParse(coin.balance) ?? Decimal.zero; } WithdrawState copyWith({ int? tabIndex, List? networkList, int? selectedNetworkId, List? onchainSubCoins, bool? onchainSubCoinsLoading, int? selectedSubCoinIndex, List? transferMainCoins, int? selectedTransferCoinIndex, List? coinInfoList, WithdrawBalance? balance, AccountAuth? auth, int? codeCountdown, bool? isSubmitting, bool? isLoading, String? errorMessage, }) => WithdrawState( tabIndex: tabIndex ?? this.tabIndex, networkList: networkList ?? this.networkList, selectedNetworkId: selectedNetworkId ?? this.selectedNetworkId, onchainSubCoins: onchainSubCoins ?? this.onchainSubCoins, onchainSubCoinsLoading: onchainSubCoinsLoading ?? this.onchainSubCoinsLoading, selectedSubCoinIndex: selectedSubCoinIndex ?? this.selectedSubCoinIndex, transferMainCoins: transferMainCoins ?? this.transferMainCoins, selectedTransferCoinIndex: selectedTransferCoinIndex ?? this.selectedTransferCoinIndex, coinInfoList: coinInfoList ?? this.coinInfoList, balance: balance ?? this.balance, auth: auth ?? this.auth, 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); try { final dio = ref.read(dioClientProvider); final withdrawService = WithdrawService(dio); final results = await Future.wait([ withdrawService.getBalance('usdt'), withdrawService.getSecuritySetting(), withdrawService.getSupportCoinInfo(), withdrawService.getWithdrawNetworks(), ]); final balance = results[0] as WithdrawBalance; final auth = results[1] as AccountAuth; final coinInfoList = results[2] as List; final networkList = results[3] as List; final transferMainCoins = await withdrawService.getTransferParents(coinInfoList: coinInfoList); state = state.copyWith( balance: balance, auth: auth, coinInfoList: coinInfoList, networkList: networkList, transferMainCoins: transferMainCoins, isLoading: false, ); await _resetSelection(); } catch (e) { state = state.copyWith( isLoading: false, errorMessage: extractErrorMessage(e), ); } } Future _resetSelection() async { final nets = state.networkList; var networkId = state.selectedNetworkId; if (nets.isEmpty) { networkId = 0; state = state.copyWith( selectedNetworkId: 0, onchainSubCoins: [], selectedSubCoinIndex: 0, ); } else { if (!nets.any((n) => n.id == networkId)) { networkId = nets.first.id; } state = state.copyWith( selectedNetworkId: networkId, selectedSubCoinIndex: 0, ); await _loadOnchainSubCoins(networkId); } var transferIdx = state.selectedTransferCoinIndex; if (transferIdx >= state.transferMainCoins.length) { transferIdx = 0; state = state.copyWith(selectedTransferCoinIndex: transferIdx); } } Future _loadOnchainSubCoins(int networkId) async { if (networkId <= 0) { state = state.copyWith(onchainSubCoins: []); return; } state = state.copyWith(onchainSubCoinsLoading: true); try { final dio = ref.read(dioClientProvider); final coins = await WithdrawService(dio).getWithdrawConfigs( networkId: networkId, coinInfoList: state.coinInfoList, ); var subIdx = state.selectedSubCoinIndex; if (subIdx >= coins.length) { subIdx = 0; } state = state.copyWith( onchainSubCoins: coins, selectedSubCoinIndex: subIdx, onchainSubCoinsLoading: false, ); } catch (e) { state = state.copyWith( onchainSubCoins: [], onchainSubCoinsLoading: false, errorMessage: extractErrorMessage(e), ); } } Future refresh() => _load(); void setTab(int index) { state = state.copyWith(tabIndex: index); } Future selectNetwork(int networkId) async { if (networkId <= 0 || !state.networkList.any((n) => n.id == networkId)) { return; } state = state.copyWith( selectedNetworkId: networkId, selectedSubCoinIndex: 0, ); await _loadOnchainSubCoins(networkId); } void selectSubCoin(int index) { if (index >= 0 && index < state.onchainSubCoins.length) { state = state.copyWith(selectedSubCoinIndex: index); } } void selectTransferCoin(int index) { if (index >= 0 && index < state.transferMainCoins.length) { state = state.copyWith(selectedTransferCoinIndex: index); } } Future sendEmailCode({ required String address, required String amount, }) async { if (state.tabIndex == 0 && state.onchainSelectedCoin == null) { return 'errSelectNetwork'; } if (state.tabIndex == 1 && state.transferSelectedCoin == null) { return 'errSelectCoin'; } if (address.isEmpty) { return 'errEnterAddress'; } if (amount.isEmpty) { return 'errEnterAmount'; } try { final dio = ref.read(dioClientProvider); final unit = state.tabIndex == 1 ? state.transferSubmitUnit : state.onChainSubmitUnit; await WithdrawService(dio).sendWithdrawEmailCode( unit: unit, address: address, amount: amount, ); 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.onChainSubmitUnit, amount: amount, address: address, fee: state.fee.toString(), vcode: vcode, jyPassword: jyPassword, vcode2: vcode2, ); state = state.copyWith(isSubmitting: false); await _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: state.transferSubmitUnit, amount: amount, address: address, vcode: vcode, jyPassword: jyPassword, vcode2: vcode2, ); state = state.copyWith(isSubmitting: false); await _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.onchainSelectedCoin == null) { return 'errSelectNetwork'; } if (state.tabIndex == 1 && state.transferSelectedCoin == null) { return 'errSelectCoin'; } 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) { final coinSymbol = state.tabIndex == 0 ? state.onchainCoinSymbol : state.transferCoinSymbol; return state.tabIndex == 0 ? 'errMinWithdraw:${state.currentMinAmount}:$coinSymbol' : 'errMinTransfer:${state.currentMinAmount}:$coinSymbol'; } 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)); } } 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, );