From e636d98fc07d2aec4803686efb7400fc5552eaf5 Mon Sep 17 00:00:00 2001 From: Beast Date: Mon, 1 Jun 2026 18:25:06 +0800 Subject: [PATCH 1/3] fix: fetching fee UX --- .../v2/screens/send/input_amount_screen.dart | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/mobile-app/lib/v2/screens/send/input_amount_screen.dart b/mobile-app/lib/v2/screens/send/input_amount_screen.dart index d87df17e..b124cb5c 100644 --- a/mobile-app/lib/v2/screens/send/input_amount_screen.dart +++ b/mobile-app/lib/v2/screens/send/input_amount_screen.dart @@ -54,6 +54,7 @@ class _InputAmountScreenState extends ConsumerState { BigInt _networkFee = BigInt.zero; int _blockHeight = 0; bool _isFetchingFee = true; + bool _isFeeStale = true; AmountInputLogic get _amountInputLogic => AmountInputLogic( exchangeRateService: ref.read(exchangeRateServiceProvider), @@ -122,7 +123,10 @@ class _InputAmountScreenState extends ConsumerState { void _onAmountChanged(String _) { final isFlipped = widget.isPayMode ? false : ref.read(isCurrencyFlippedProvider); try { - setState(() => _amount = _amountInputLogic.onAmountChanged(value: _amountController.text, isFlipped: isFlipped)); + setState(() { + _amount = _amountInputLogic.onAmountChanged(value: _amountController.text, isFlipped: isFlipped); + _isFeeStale = true; + }); } on InvalidNumberInputException catch (e, stack) { debugPrint('Amount parse failed: $e\n$stack'); final l10n = ref.read(l10nProvider); @@ -152,7 +156,12 @@ class _InputAmountScreenState extends ConsumerState { } catch (e) { debugPrint('Estimated fee fetch error: $e'); } finally { - if (mounted) setState(() => _isFetchingFee = false); + if (mounted) { + setState(() { + _isFetchingFee = false; + _isFeeStale = false; + }); + } } } @@ -176,7 +185,12 @@ class _InputAmountScreenState extends ConsumerState { } catch (e) { debugPrint('Fee fetch error: $e'); } finally { - if (mounted) setState(() => _isFetchingFee = false); + if (mounted) { + setState(() { + _isFetchingFee = false; + _isFeeStale = false; + }); + } } } @@ -189,7 +203,10 @@ class _InputAmountScreenState extends ConsumerState { _amountController.text = isFlipped ? _amountInputLogic.quanToFiatString(max) : _amountInputLogic.formatQuanAmount(max); - setState(() => _amount = max); + setState(() { + _amount = max; + _isFeeStale = true; + }); if (max > BigInt.zero) _fetchFee(); } @@ -242,6 +259,7 @@ class _InputAmountScreenState extends ConsumerState { final amountStatus = SendScreenLogic.getAmountStatus(_amount, balance.value ?? BigInt.zero, _networkFee); final btnDisabled = _isFetchingFee || + _isFeeStale || _recipientChecksum == null || SendScreenLogic.isButtonDisabled( hasAddressError: false, @@ -483,7 +501,7 @@ class _InputAmountScreenState extends ConsumerState { style: text.smallParagraph?.copyWith(color: colors.textTertiary), ), const SizedBox(height: 4), - if (!_isFetchingFee) + if (!_isFetchingFee && !_isFeeStale) Text( l10n.commonAmountBalance( formattingService.formatBalance(_networkFee, maxDecimals: 5), From d5f55155b61ef430b8528c008d14b1f73326bf88 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Tue, 2 Jun 2026 00:22:31 +0800 Subject: [PATCH 2/3] redo send flow --- mobile-app/lib/l10n/app_en.arb | 2 +- mobile-app/lib/l10n/app_id.arb | 2 +- mobile-app/lib/l10n/app_localizations.dart | 2 +- mobile-app/lib/l10n/app_localizations_en.dart | 2 +- mobile-app/lib/l10n/app_localizations_id.dart | 2 +- .../v2/screens/send/input_amount_screen.dart | 83 +++++++------------ .../screens/send/select_recipient_screen.dart | 22 ++++- 7 files changed, 54 insertions(+), 61 deletions(-) diff --git a/mobile-app/lib/l10n/app_en.arb b/mobile-app/lib/l10n/app_en.arb index 737f9982..6142f563 100644 --- a/mobile-app/lib/l10n/app_en.arb +++ b/mobile-app/lib/l10n/app_en.arb @@ -388,7 +388,7 @@ "@sendSelectRecipientSendTo": { "description": "Section label on select recipient screen" }, - "sendSelectRecipientSearchHint": "Search {symbol} Address", + "sendSelectRecipientSearchHint": "Enter {symbol} Address", "@sendSelectRecipientSearchHint": { "description": "Hint for recipient search field", "placeholders": { diff --git a/mobile-app/lib/l10n/app_id.arb b/mobile-app/lib/l10n/app_id.arb index 73450e04..be8b43c2 100644 --- a/mobile-app/lib/l10n/app_id.arb +++ b/mobile-app/lib/l10n/app_id.arb @@ -102,7 +102,7 @@ "sendEnterAddress": "Masukkan Alamat", "sendSelectRecipientSendTo": "Kirim Ke", - "sendSelectRecipientSearchHint": "Cari Alamat {symbol}", + "sendSelectRecipientSearchHint": "Masukkan Alamat {symbol}", "sendSelectRecipientScanTitle": "Pindai kode QR", "sendSelectRecipientScanSubtitle": "Ketuk untuk memindai Alamat {symbol}", "sendSelectRecipientRecents": "Terbaru", diff --git a/mobile-app/lib/l10n/app_localizations.dart b/mobile-app/lib/l10n/app_localizations.dart index dcd5b871..9c3162c5 100644 --- a/mobile-app/lib/l10n/app_localizations.dart +++ b/mobile-app/lib/l10n/app_localizations.dart @@ -611,7 +611,7 @@ abstract class AppLocalizations { /// Hint for recipient search field /// /// In en, this message translates to: - /// **'Search {symbol} Address'** + /// **'Enter {symbol} Address'** String sendSelectRecipientSearchHint(String symbol); /// Scan QR row title diff --git a/mobile-app/lib/l10n/app_localizations_en.dart b/mobile-app/lib/l10n/app_localizations_en.dart index 0d497542..dc65d30f 100644 --- a/mobile-app/lib/l10n/app_localizations_en.dart +++ b/mobile-app/lib/l10n/app_localizations_en.dart @@ -281,7 +281,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String sendSelectRecipientSearchHint(String symbol) { - return 'Search $symbol Address'; + return 'Enter $symbol Address'; } @override diff --git a/mobile-app/lib/l10n/app_localizations_id.dart b/mobile-app/lib/l10n/app_localizations_id.dart index f35b2555..f8b76280 100644 --- a/mobile-app/lib/l10n/app_localizations_id.dart +++ b/mobile-app/lib/l10n/app_localizations_id.dart @@ -282,7 +282,7 @@ class AppLocalizationsId extends AppLocalizations { @override String sendSelectRecipientSearchHint(String symbol) { - return 'Cari Alamat $symbol'; + return 'Masukkan Alamat $symbol'; } @override diff --git a/mobile-app/lib/v2/screens/send/input_amount_screen.dart b/mobile-app/lib/v2/screens/send/input_amount_screen.dart index b124cb5c..7e11aeaa 100644 --- a/mobile-app/lib/v2/screens/send/input_amount_screen.dart +++ b/mobile-app/lib/v2/screens/send/input_amount_screen.dart @@ -49,12 +49,14 @@ class _InputAmountScreenState extends ConsumerState { final _feeDebouncer = Debouncer(delay: const Duration(milliseconds: 500)); + static final BigInt _estimateFeeAmount = BigInt.from(1000) * NumberFormattingService.scaleFactorBigInt; + String? _recipientChecksum; BigInt _amount = BigInt.zero; BigInt _networkFee = BigInt.zero; int _blockHeight = 0; - bool _isFetchingFee = true; - bool _isFeeStale = true; + bool _isFetchingFee = false; + bool _hasFee = false; AmountInputLogic get _amountInputLogic => AmountInputLogic( exchangeRateService: ref.read(exchangeRateServiceProvider), @@ -86,9 +88,7 @@ class _InputAmountScreenState extends ConsumerState { }); } - WidgetsBinding.instance.addPostFrameCallback((_) { - _fetchEstimatedFee(); - }); + _refreshFee(); } @override @@ -123,74 +123,53 @@ class _InputAmountScreenState extends ConsumerState { void _onAmountChanged(String _) { final isFlipped = widget.isPayMode ? false : ref.read(isCurrencyFlippedProvider); try { - setState(() { - _amount = _amountInputLogic.onAmountChanged(value: _amountController.text, isFlipped: isFlipped); - _isFeeStale = true; - }); + setState(() => _amount = _amountInputLogic.onAmountChanged(value: _amountController.text, isFlipped: isFlipped)); } on InvalidNumberInputException catch (e, stack) { debugPrint('Amount parse failed: $e\n$stack'); final l10n = ref.read(l10nProvider); context.showErrorToaster(message: l10n.sendInputAmountInvalidAmount); return; } - if (_amount > BigInt.zero) _feeDebouncer.run(_fetchFee); + _feeDebouncer.run(_refreshFee); + } + + void _refreshFee() { + final recipient = widget.recipientAddress.trim(); + if (_amount > BigInt.zero && ref.read(substrateServiceProvider).isValidSS58Address(recipient)) { + _fetchFee(_amount, recipient); + } else { + _fetchEstimatedFee(); + } } Future _fetchEstimatedFee() async { final displayAccount = ref.read(activeAccountProvider).value; if (displayAccount is! RegularAccount) return; - final account = displayAccount.account; - try { - final balancesService = ref.read(balancesServiceProvider); - final formattingService = ref.read(numberFormattingServiceProvider); - final feeData = await balancesService.getBalanceTransferFee( - account, - account.accountId, - formattingService.parseAmount('1000') ?? BigInt.zero, - ); - if (!mounted) return; - setState(() { - _networkFee = feeData.fee; - _blockHeight = feeData.blockNumber; - }); - } catch (e) { - debugPrint('Estimated fee fetch error: $e'); - } finally { - if (mounted) { - setState(() { - _isFetchingFee = false; - _isFeeStale = false; - }); - } - } + _fetchFee(_estimateFeeAmount, displayAccount.account.accountId); } - Future _fetchFee() async { + Future _fetchFee(BigInt amount, String toAddress) async { if (_isFetchingFee) return; - setState(() => _isFetchingFee = true); + final displayAccount = ref.read(activeAccountProvider).value; + if (displayAccount is! RegularAccount) return; + _isFetchingFee = true; try { - final displayAccount = ref.read(activeAccountProvider).value; - if (displayAccount is! RegularAccount) return; final balancesService = ref.read(balancesServiceProvider); final feeData = await balancesService.getBalanceTransferFee( displayAccount.account, - widget.recipientAddress.trim(), - _amount, + toAddress, + amount, ); if (!mounted) return; setState(() { _networkFee = feeData.fee; _blockHeight = feeData.blockNumber; + _hasFee = true; }); } catch (e) { debugPrint('Fee fetch error: $e'); } finally { - if (mounted) { - setState(() { - _isFetchingFee = false; - _isFeeStale = false; - }); - } + if (mounted) setState(() => _isFetchingFee = false); } } @@ -203,11 +182,8 @@ class _InputAmountScreenState extends ConsumerState { _amountController.text = isFlipped ? _amountInputLogic.quanToFiatString(max) : _amountInputLogic.formatQuanAmount(max); - setState(() { - _amount = max; - _isFeeStale = true; - }); - if (max > BigInt.zero) _fetchFee(); + setState(() => _amount = max); + _refreshFee(); } Future _toggleFlip() async { @@ -258,8 +234,7 @@ class _InputAmountScreenState extends ConsumerState { final amountStatus = SendScreenLogic.getAmountStatus(_amount, balance.value ?? BigInt.zero, _networkFee); final btnDisabled = - _isFetchingFee || - _isFeeStale || + !_hasFee || _recipientChecksum == null || SendScreenLogic.isButtonDisabled( hasAddressError: false, @@ -501,7 +476,7 @@ class _InputAmountScreenState extends ConsumerState { style: text.smallParagraph?.copyWith(color: colors.textTertiary), ), const SizedBox(height: 4), - if (!_isFetchingFee && !_isFeeStale) + if (_hasFee) Text( l10n.commonAmountBalance( formattingService.formatBalance(_networkFee, maxDecimals: 5), diff --git a/mobile-app/lib/v2/screens/send/select_recipient_screen.dart b/mobile-app/lib/v2/screens/send/select_recipient_screen.dart index 3d908b5f..ae2e7d07 100644 --- a/mobile-app/lib/v2/screens/send/select_recipient_screen.dart +++ b/mobile-app/lib/v2/screens/send/select_recipient_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/dotted_border.dart'; @@ -180,6 +181,15 @@ class _SelectRecipientScreenState extends ConsumerState { _recipientController.text = address; } + Future _pasteRecipient() async { + final data = await Clipboard.getData(Clipboard.kTextPlain); + final text = data?.text?.trim() ?? ''; + if (text.isEmpty) return; + _amountController.clear(); + setState(() => _isPayMode = false); + _recipientController.text = text; + } + @override Widget build(BuildContext context) { final l10n = ref.watch(l10nProvider); @@ -270,7 +280,6 @@ class _SelectRecipientScreenState extends ConsumerState { decoration: BoxDecoration(color: colors.sheetBackground, borderRadius: BorderRadius.circular(8)), child: Row( children: [ - Icon(Icons.search, size: 14, color: colors.textLabel), const SizedBox(width: 12), Expanded( child: TextField( @@ -288,6 +297,15 @@ class _SelectRecipientScreenState extends ConsumerState { ), ), ), + IconButton( + onPressed: _pasteRecipient, + icon: const Icon(Icons.paste), + iconSize: 20, + color: colors.textPrimary, + padding: EdgeInsets.zero, + visualDensity: VisualDensity.compact, + constraints: const BoxConstraints(minWidth: 40, minHeight: 40), + ), ], ), ), @@ -309,7 +327,7 @@ class _SelectRecipientScreenState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - AddressFormattingService.formatAddress(_recipientController.text.trim()), + AddressFormattingService.formatAddress(prefix: 16, postFix: 16, _recipientController.text.trim()), style: text.smallParagraph?.copyWith(color: colors.textPrimary, fontWeight: FontWeight.w500), maxLines: 1, overflow: TextOverflow.ellipsis, From 79e6b904741a105641d65f7e393704131bab476d Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 2 Jun 2026 11:05:18 +0800 Subject: [PATCH 3/3] chore: formatting --- mobile-app/lib/v2/screens/send/input_amount_screen.dart | 6 +----- mobile-app/lib/v2/screens/send/select_recipient_screen.dart | 6 +++++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mobile-app/lib/v2/screens/send/input_amount_screen.dart b/mobile-app/lib/v2/screens/send/input_amount_screen.dart index 7e11aeaa..2987bd77 100644 --- a/mobile-app/lib/v2/screens/send/input_amount_screen.dart +++ b/mobile-app/lib/v2/screens/send/input_amount_screen.dart @@ -155,11 +155,7 @@ class _InputAmountScreenState extends ConsumerState { _isFetchingFee = true; try { final balancesService = ref.read(balancesServiceProvider); - final feeData = await balancesService.getBalanceTransferFee( - displayAccount.account, - toAddress, - amount, - ); + final feeData = await balancesService.getBalanceTransferFee(displayAccount.account, toAddress, amount); if (!mounted) return; setState(() { _networkFee = feeData.fee; diff --git a/mobile-app/lib/v2/screens/send/select_recipient_screen.dart b/mobile-app/lib/v2/screens/send/select_recipient_screen.dart index ae2e7d07..fd5cb667 100644 --- a/mobile-app/lib/v2/screens/send/select_recipient_screen.dart +++ b/mobile-app/lib/v2/screens/send/select_recipient_screen.dart @@ -327,7 +327,11 @@ class _SelectRecipientScreenState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - AddressFormattingService.formatAddress(prefix: 16, postFix: 16, _recipientController.text.trim()), + AddressFormattingService.formatAddress( + prefix: 16, + postFix: 16, + _recipientController.text.trim(), + ), style: text.smallParagraph?.copyWith(color: colors.textPrimary, fontWeight: FontWeight.w500), maxLines: 1, overflow: TextOverflow.ellipsis,