From 9b0762ff4d8aa06e12b981190e024ba64e62d962 Mon Sep 17 00:00:00 2001 From: joshuakrueger-dfx Date: Wed, 19 Aug 2026 16:24:49 +0200 Subject: [PATCH 01/18] fix(buy,sell): tell the user an amount is ambiguous instead of failing generically An amount typed with a thousands separator (1.000) is deliberately rejected by chargedFiatAmount, which throws a FormatException. Both payment-info cubits caught it in their generic catch and surfaced PaymentInfoError.unknown, so the user was told to contact support instead of being told to drop the separator. Give the rejection its own error value and its own message. The parsing rule itself is unchanged: a grouping-ambiguous amount is still refused rather than guessed. --- assets/languages/strings_de.arb | 2 + assets/languages/strings_en.arb | 2 + .../models/payment/payment_info_error.dart | 1 + .../buy_payment_info_cubit.dart | 2 + .../buy/widgets/payment_information.dart | 5 + .../sell_payment_info_cubit.dart | 8 + lib/screens/sell/widgets/sell_button.dart | 11 + .../models/payment/buy_sell_dtos_test.dart | 5 +- .../buy/buy_amount_change_repro_test.dart | 342 ++++++++++++++++++ .../cubits/buy_payment_info_cubit_test.dart | 12 + .../cubits/sell_payment_info_cubit_test.dart | 12 + 11 files changed, 400 insertions(+), 2 deletions(-) create mode 100644 test/screens/buy/buy_amount_change_repro_test.dart diff --git a/assets/languages/strings_de.arb b/assets/languages/strings_de.arb index b69627dd7..208535220 100644 --- a/assets/languages/strings_de.arb +++ b/assets/languages/strings_de.arb @@ -111,6 +111,8 @@ "identityCheckProcessDescription": "Als nächstes müssen Sie Ihre Identität verifizieren. Bitte halten Sie Ihren Ausweis bereit und erlauben Sie den Kamerazugriff auf dem Gerät.", "identityCheckRequired": "Identitätsprüfung erforderlich", "imprint": "Impressum", + "invalidAmountFormatDescription": "Bitte geben Sie den Betrag ohne Tausenderpunkt ein, zum Beispiel 1000 statt 1.000.", + "invalidAmountFormatTitle": "Betrag nicht eindeutig", "kyc": "Eröffnungsprozess", "kycAccountMergeDescription": "Ihre Identität wurde bereits in einem anderen Konto gefunden. Eine Zusammenführungsanfrage wurde erstellt. Bitte bestätigen Sie diese über die E-Mail, die Sie erhalten haben.", "kycAccountMergeTitle": "Kontozusammenführung erforderlich", diff --git a/assets/languages/strings_en.arb b/assets/languages/strings_en.arb index 58a7d2b84..4c3ad8105 100644 --- a/assets/languages/strings_en.arb +++ b/assets/languages/strings_en.arb @@ -111,6 +111,8 @@ "identityCheckProcessDescription": "Next, you need to verify your identity. Please have your ID ready and allow camera access on your device.", "identityCheckRequired": "Identity check required", "imprint": "Imprint", + "invalidAmountFormatDescription": "Please enter the amount without a thousands separator, for example 1000 instead of 1.000.", + "invalidAmountFormatTitle": "Amount is ambiguous", "kyc": "Onboarding process", "kycAccountMergeDescription": "Your identity was found in another account. A merge request has been created. Please confirm it via the email you received.", "kycAccountMergeTitle": "Account merge required", diff --git a/lib/packages/service/dfx/models/payment/payment_info_error.dart b/lib/packages/service/dfx/models/payment/payment_info_error.dart index 42409bbe6..ccf686d91 100644 --- a/lib/packages/service/dfx/models/payment/payment_info_error.dart +++ b/lib/packages/service/dfx/models/payment/payment_info_error.dart @@ -7,4 +7,5 @@ enum PaymentInfoError { bitboxDisconnected, priceSourceUnavailable, unknown, + invalidAmountFormat, } diff --git a/lib/screens/buy/cubits/buy_payment_info/buy_payment_info_cubit.dart b/lib/screens/buy/cubits/buy_payment_info/buy_payment_info_cubit.dart index e97acfa99..8fd155a80 100644 --- a/lib/screens/buy/cubits/buy_payment_info/buy_payment_info_cubit.dart +++ b/lib/screens/buy/cubits/buy_payment_info/buy_payment_info_cubit.dart @@ -113,6 +113,8 @@ class BuyPaymentInfoCubit extends Cubit { } developer.log(e.toString()); return BuyPaymentInfoFailure(PaymentInfoError.unknown, message: e.message); + } on FormatException { + return const BuyPaymentInfoFailure(PaymentInfoError.invalidAmountFormat); } catch (e) { developer.log(e.toString()); return BuyPaymentInfoFailure(PaymentInfoError.unknown, message: e.toString()); diff --git a/lib/screens/buy/widgets/payment_information.dart b/lib/screens/buy/widgets/payment_information.dart index dd1908b55..3312fdfa5 100644 --- a/lib/screens/buy/widgets/payment_information.dart +++ b/lib/screens/buy/widgets/payment_information.dart @@ -37,6 +37,11 @@ class PaymentInformation extends StatelessWidget { title: S.of(context).bitboxDisconnectedTitle, description: S.of(context).bitboxDisconnectedDescription, ); + } else if (error == PaymentInfoError.invalidAmountFormat) { + return PaymentActionRequired( + title: S.of(context).invalidAmountFormatTitle, + description: S.of(context).invalidAmountFormatDescription, + ); } else if (error == PaymentInfoError.priceSourceUnavailable || error == PaymentInfoError.unknown) { if (paymentInfoState.message.isEmpty) { diff --git a/lib/screens/sell/cubits/sell_payment_info/sell_payment_info_cubit.dart b/lib/screens/sell/cubits/sell_payment_info/sell_payment_info_cubit.dart index 2c4ba5d82..a7ee76c1c 100644 --- a/lib/screens/sell/cubits/sell_payment_info/sell_payment_info_cubit.dart +++ b/lib/screens/sell/cubits/sell_payment_info/sell_payment_info_cubit.dart @@ -115,6 +115,14 @@ class SellPaymentInfoCubit extends Cubit { message: e.message, ), ); + } on FormatException catch (e) { + if (isClosed) return; + emit( + SellPaymentInfoFailure( + PaymentInfoError.invalidAmountFormat, + message: e.toString(), + ), + ); } catch (e) { developer.log(e.toString()); if (isClosed) return; diff --git a/lib/screens/sell/widgets/sell_button.dart b/lib/screens/sell/widgets/sell_button.dart index 81dcdca74..2bc3c5dec 100644 --- a/lib/screens/sell/widgets/sell_button.dart +++ b/lib/screens/sell/widgets/sell_button.dart @@ -44,6 +44,17 @@ class SellButton extends StatelessWidget { } return; } + if (state.error == .invalidAmountFormat) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(S.of(context).invalidAmountFormatDescription), + backgroundColor: RealUnitColors.status.red600, + ), + ); + } + return; + } if (context.mounted && state.message.isNotEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/test/packages/service/dfx/models/payment/buy_sell_dtos_test.dart b/test/packages/service/dfx/models/payment/buy_sell_dtos_test.dart index 7d8889c71..7288cda44 100644 --- a/test/packages/service/dfx/models/payment/buy_sell_dtos_test.dart +++ b/test/packages/service/dfx/models/payment/buy_sell_dtos_test.dart @@ -182,9 +182,9 @@ void main() { }); group('$PaymentInfoError', () { - test('has the eight documented variants', () { + test('has the nine documented variants', () { // Pin the wire contract — any new variant has to be added intentionally. - expect(PaymentInfoError.values, hasLength(8)); + expect(PaymentInfoError.values, hasLength(9)); expect( PaymentInfoError.values.toSet(), { @@ -196,6 +196,7 @@ void main() { PaymentInfoError.bitboxDisconnected, PaymentInfoError.priceSourceUnavailable, PaymentInfoError.unknown, + PaymentInfoError.invalidAmountFormat, }, ); }); diff --git a/test/screens/buy/buy_amount_change_repro_test.dart b/test/screens/buy/buy_amount_change_repro_test.dart new file mode 100644 index 000000000..ace565860 --- /dev/null +++ b/test/screens/buy/buy_amount_change_repro_test.dart @@ -0,0 +1,342 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:realunit_wallet/generated/i18n.dart'; +import 'package:realunit_wallet/packages/config/api_config.dart'; +import 'package:realunit_wallet/packages/repository/cache_repository.dart'; +import 'package:realunit_wallet/packages/repository/supported_fiat_repository.dart'; +import 'package:realunit_wallet/packages/service/app_store.dart'; +import 'package:realunit_wallet/packages/service/dfx/dfx_brokerbot_service.dart'; +import 'package:realunit_wallet/packages/service/dfx/dfx_price_service.dart'; +import 'package:realunit_wallet/packages/service/dfx/models/brokerbot/dfx_buy_price_dto.dart'; +import 'package:realunit_wallet/packages/service/dfx/models/brokerbot/dfx_buy_shares_dto.dart'; +import 'package:realunit_wallet/packages/service/dfx/models/payment/buy/buy_payment_info.dart'; +import 'package:realunit_wallet/packages/service/dfx/real_unit_buy_payment_info_service.dart'; +import 'package:realunit_wallet/packages/service/session_cache.dart'; +import 'package:realunit_wallet/packages/utils/fiat_amount.dart'; +import 'package:realunit_wallet/screens/buy/buy_page.dart'; +import 'package:realunit_wallet/screens/buy/cubits/buy_converter/buy_converter_cubit.dart'; +import 'package:realunit_wallet/screens/buy/cubits/buy_payment_info/buy_payment_info_cubit.dart'; +import 'package:realunit_wallet/screens/buy/widgets/payment_action_required.dart'; +import 'package:realunit_wallet/styles/currency.dart'; + +import '../../helper/helper.dart'; + +class MockDfxBrokerbotService extends Mock implements DfxBrokerbotService {} + +class MockRealUnitBuyPaymentInfoService extends Mock + implements RealUnitBuyPaymentInfoService {} + +class MockDfxPriceService extends Mock implements DFXPriceService {} + +class MockApiConfig extends Mock implements ApiConfig {} + +class MockCacheRepository extends Mock implements CacheRepository {} + +class MockSupportedFiatRepository extends Mock implements SupportedFiatRepository {} + +/// Matches [BuyConverterCubit]'s 100 ms debounce, plus a little slack so the +/// timer body (brokerbot call → loading:false → payment-info fetch) can finish. +const _afterDebounce = Duration(milliseconds: 150); + +/// Production-like floor used when the quote returns `AmountTooLow`. +const _minVolume = 100.0; + +void main() { + late MockDfxBrokerbotService brokerbotService; + late MockRealUnitBuyPaymentInfoService paymentInfoService; + + void setupDependencyInjection() { + final getIt = GetIt.instance; + brokerbotService = MockDfxBrokerbotService(); + paymentInfoService = MockRealUnitBuyPaymentInfoService(); + + getIt.registerSingleton( + AppStore(() => MockApiConfig(), SessionCache(MockCacheRepository())), + ); + getIt.registerSingleton(brokerbotService); + getIt.registerSingleton(paymentInfoService); + getIt.registerSingleton(MockDfxPriceService()); + final fiatRepo = MockSupportedFiatRepository(); + when(() => fiatRepo.getBuyable()).thenAnswer((_) async => const [Currency.chf, Currency.eur]); + when(() => fiatRepo.getSellable()).thenAnswer((_) async => const [Currency.chf]); + when(() => fiatRepo.getAll()).thenAnswer((_) async => const [Currency.chf, Currency.eur]); + getIt.registerSingleton(fiatRepo); + } + + setUpAll(() { + registerFallbackValue(Currency.chf); + setupDependencyInjection(); + }); + + tearDownAll(() async => await GetIt.instance.reset()); + + setUp(() { + reset(brokerbotService); + reset(paymentInfoService); + _stubProductionLikeQuotes(brokerbotService, paymentInfoService); + }); + + group('BuyPage amount change (customer reproduction)', () { + testWidgets( + 'character-by-character replace of 300 with 100 does not show the support-contact error', + (tester) async { + await _pumpLoadedBuyPage(tester); + + for (final value in ['30', '3', '', '1', '10', '100']) { + await _enterAmount(tester, value); + } + + _expectHealthyQuote( + tester, + expectedAmount: '100', + reasonPrefix: + 'After deleting 300 digit-by-digit and typing 100, the screen must ' + 'keep a valid quote. Intermediate 0–3 are AmountTooLow only; 100 ' + 'is a valid quote in production.', + ); + }, + ); + + testWidgets( + 'replacing 300 with 100 in one step does not show the support-contact error', + (tester) async { + await _pumpLoadedBuyPage(tester); + + await _enterAmount(tester, ''); + await _enterAmount(tester, '100'); + + _expectHealthyQuote( + tester, + expectedAmount: '100', + reasonPrefix: + 'After clearing the default 300 and typing 100 in one step, the ' + 'screen must keep a valid quote. The quote API accepts 100.', + ); + }, + ); + + testWidgets( + 'typing 1.000 (thousands grouping) does not show the support-contact error', + (tester) async { + await _pumpLoadedBuyPage(tester); + + await _enterAmount(tester, '1.000'); + + final snapshot = _snapshot(tester); + final amount = tester.widget(_amountField); + + expect( + find.text(S.current.invalidAmountFormatTitle), + findsOneWidget, + reason: + 'Typing 1.000 (grouping-ambiguous) must show the specific ' + 'ambiguous-amount hint. $snapshot', + ); + expect( + find.text(S.current.invalidAmountFormatDescription), + findsOneWidget, + reason: + 'Typing 1.000 (grouping-ambiguous) must show the specific ' + 'ambiguous-amount description. $snapshot', + ); + expect( + find.text(S.current.paymentInformationFailed), + findsNothing, + reason: + 'Typing 1.000 (grouping-ambiguous) must not become the generic ' + 'support-contact error. $snapshot', + ); + expect( + find.text(S.current.paymentInformationFailedDescription), + findsNothing, + reason: + 'Typing 1.000 (grouping-ambiguous) must not show the support ' + 'description. $snapshot', + ); + expect( + find.byType(CupertinoActivityIndicator), + findsNothing, + reason: 'Payment info is still spinning. $snapshot', + ); + expect( + amount.controller!.text, + '1.000', + reason: 'Amount field is not "1.000". $snapshot', + ); + }, + ); + }); +} + +void _stubProductionLikeQuotes( + MockDfxBrokerbotService brokerbot, + MockRealUnitBuyPaymentInfoService paymentInfo, +) { + // Real DfxBrokerbotService rejects unparseable / non-positive input + // (empty, "1.000") and otherwise returns a conversion. Amounts 1–3 are + // convertible; the min-amount gate lives on the quote, not here. + when(() => brokerbot.getBuyShares(any(), any())).thenAnswer((invocation) async { + final raw = invocation.positionalArguments[0] as String; + final parsed = tryParseFiatAmount(raw); + if (parsed == null || parsed <= 0) { + throw Exception('Shares request failed: amountInput is not valid'); + } + return BrokerbotBuySharesDto( + shares: parsed < 1.43 ? 1 : parsed ~/ 1.43, + pricePerShare: 1.43, + availableShares: 100000, + ); + }); + + when(() => brokerbot.getBuyPrice(any(), any())).thenAnswer((invocation) async { + final raw = invocation.positionalArguments[0] as String; + final shares = int.tryParse(raw); + if (shares == null || shares <= 0) { + throw Exception('BuyPrice request failed: sharesInput is not valid'); + } + return BrokerbotBuyPriceDto( + totalCost: shares * 1.43, + pricePerShare: 1.43, + availableShares: 100000, + ); + }); + + when( + () => paymentInfo.getPaymentInfo(any(), currency: any(named: 'currency')), + ).thenAnswer((invocation) async { + final amount = invocation.positionalArguments[0] as int; + final currency = + invocation.namedArguments[#currency] as Currency? ?? Currency.chf; + if (amount >= 4) { + return _quote(amount: amount, currency: currency, isValid: true); + } + return _quote( + amount: amount, + currency: currency, + isValid: false, + error: 'AmountTooLow', + minVolume: _minVolume, + ); + }); +} + +BuyPaymentInfo _quote({ + required int amount, + required Currency currency, + required bool isValid, + String? error, + double? minVolume, +}) { + return BuyPaymentInfo( + id: 1, + iban: 'CH56 0483 5012 3456 78', + bic: 'CRESCHZZ80A', + name: 'DFX AG', + street: 'Bahnhofstrasse', + number: '1', + zip: '8000', + city: 'Zurich', + country: 'CH', + currency: currency, + amount: amount.toDouble(), + isValid: isValid, + error: error, + minVolume: minVolume, + ); +} + +Finder get _amountField => find.byType(TextField).first; + +Future _pumpLoadedBuyPage(WidgetTester tester) async { + await tester.pumpApp(const BuyPage()); + // BuyPage constructs BuyConverterCubit(..)..onFiatChanged('300'). + await tester.pump(); + await tester.pump(_afterDebounce); + await tester.pump(); + + final amount = tester.widget(_amountField); + expect( + amount.controller!.text, + '300', + reason: 'Precondition failed: default 300 did not land. ${_snapshot(tester)}', + ); + expect( + find.text(S.current.paymentInformationFailed), + findsNothing, + reason: + 'Precondition failed: support-contact error already visible on the ' + 'default 300. ${_snapshot(tester)}', + ); +} + +Future _enterAmount(WidgetTester tester, String value) async { + await tester.enterText(_amountField, value); + await tester.pump(_afterDebounce); + await tester.pump(); +} + +void _expectHealthyQuote( + WidgetTester tester, { + required String expectedAmount, + required String reasonPrefix, +}) { + final snapshot = _snapshot(tester); + final amount = tester.widget(_amountField); + + expect( + find.text(S.current.paymentInformationFailed), + findsNothing, + reason: '$reasonPrefix Support title is visible. $snapshot', + ); + expect( + find.text(S.current.paymentInformationFailedDescription), + findsNothing, + reason: '$reasonPrefix Support description is visible. $snapshot', + ); + expect( + find.byType(CupertinoActivityIndicator), + findsNothing, + reason: '$reasonPrefix Payment info is still spinning. $snapshot', + ); + expect( + amount.controller!.text, + expectedAmount, + reason: '$reasonPrefix Amount field is not "$expectedAmount". $snapshot', + ); +} + +String _snapshot(WidgetTester tester) { + final amountText = tester.widget(_amountField).controller?.text; + final viewContext = tester.element(find.byType(BuyView)); + final paymentState = viewContext.read().state; + final converterState = viewContext.read().state; + final supportVisible = find.text(S.current.paymentInformationFailed).evaluate().isNotEmpty; + final spinnerVisible = find.byType(CupertinoActivityIndicator).evaluate().isNotEmpty; + final actionRequired = find.byType(PaymentActionRequired).evaluate().isNotEmpty; + return 'amountField="$amountText" ' + 'converter(fiat=${converterState.fiatText}, shares=${converterState.sharesText}, ' + 'loading=${converterState.loading}, currency=${converterState.currency.code}) ' + 'paymentInfo=${_describePayment(paymentState)} ' + 'supportText=$supportVisible spinner=$spinnerVisible ' + 'PaymentActionRequired=$actionRequired'; +} + +String _describePayment(BuyPaymentInfoState state) { + if (state is BuyPaymentInfoSuccess) { + return 'Success(amount=${state.buyPaymentInfo.amount}, ' + 'isValid=${state.buyPaymentInfo.isValid})'; + } + if (state is BuyPaymentInfoMinAmountNotMetFailure) { + return 'MinAmountNotMet(minAmount=${state.minAmount})'; + } + if (state is BuyPaymentInfoFailure) { + return 'Failure(${state.error})'; + } + if (state is BuyPaymentInfoLoading) return 'Loading'; + if (state is BuyPaymentInfoInitial) return 'Initial'; + return state.runtimeType.toString(); +} diff --git a/test/screens/buy/cubits/buy_payment_info_cubit_test.dart b/test/screens/buy/cubits/buy_payment_info_cubit_test.dart index 486ba9242..0f656cffe 100644 --- a/test/screens/buy/cubits/buy_payment_info_cubit_test.dart +++ b/test/screens/buy/cubits/buy_payment_info_cubit_test.dart @@ -179,6 +179,18 @@ void main() { verify(() => service.getPaymentInfo(301, currency: Currency.chf)).called(1); }); + test('grouping-ambiguous amount (1.000) → Failure(invalidAmountFormat), not unknown', () async { + final cubit = build(); + await cubit.getPaymentInfo(amount: '1.000'); + + expect(cubit.state, isA()); + expect( + (cubit.state as BuyPaymentInfoFailure).error, + PaymentInfoError.invalidAmountFormat, + ); + verifyNever(() => service.getPaymentInfo(any(), currency: any(named: 'currency'))); + }); + test('KycLevelRequiredException → Failure(kycRequired, requiredLevel)', () async { when(() => service.getPaymentInfo(any(), currency: any(named: 'currency'))) .thenAnswer( diff --git a/test/screens/sell/cubits/sell_payment_info_cubit_test.dart b/test/screens/sell/cubits/sell_payment_info_cubit_test.dart index 94be1457f..9bec37020 100644 --- a/test/screens/sell/cubits/sell_payment_info_cubit_test.dart +++ b/test/screens/sell/cubits/sell_payment_info_cubit_test.dart @@ -349,6 +349,18 @@ void main() { }, ); + test('grouping-ambiguous amount (1.000) → Failure(invalidAmountFormat), not unknown', () async { + final cubit = build(); + await cubit.getPaymentInfo(amount: '1.000', iban: 'CH56'); + + expect(cubit.state, isA()); + expect( + (cubit.state as SellPaymentInfoFailure).error, + PaymentInfoError.invalidAmountFormat, + ); + verifyNever(() => service.getPaymentInfo(any(), any(), currency: any(named: 'currency'))); + }); + test('does not emit after close', () async { final completer = Completer(); when( From f9a06413f243f8b7044924657998788bdb838c96 Mon Sep 17 00:00:00 2001 From: joshuakrueger-dfx Date: Wed, 19 Aug 2026 16:24:49 +0200 Subject: [PATCH 02/18] fix(converter): drop the stale counter value when a conversion fails When the brokerbot conversion threw, both converter cubits only cleared the loading flag and left the previously computed counter value in place. The buy screen therefore showed the rejected input 1.000 next to 209 shares, a number belonging to the 300 quote before it. Clear the result field in every catch so no figure outlives the input it was computed from. The _seq race guard is untouched. --- .../buy_converter/buy_converter_cubit.dart | 6 +- .../sell_converter/sell_converter_cubit.dart | 6 +- .../buy/cubits/buy_converter_cubit_test.dart | 74 ++++++++++++++++++ .../cubits/sell_converter_cubit_test.dart | 77 +++++++++++++++++++ 4 files changed, 157 insertions(+), 6 deletions(-) diff --git a/lib/screens/buy/cubits/buy_converter/buy_converter_cubit.dart b/lib/screens/buy/cubits/buy_converter/buy_converter_cubit.dart index 8fa7ac9a8..b5200767a 100644 --- a/lib/screens/buy/cubits/buy_converter/buy_converter_cubit.dart +++ b/lib/screens/buy/cubits/buy_converter/buy_converter_cubit.dart @@ -48,7 +48,7 @@ class BuyConverterCubit extends Cubit { } catch (e) { developer.log(e.toString()); if (isClosed || mySeq != _seq) return; - emit(state.copyWith(loading: false)); + emit(state.copyWith(loading: false, sharesText: '')); } }); } @@ -75,7 +75,7 @@ class BuyConverterCubit extends Cubit { } catch (e) { developer.log(e.toString()); if (isClosed || mySeq != _seq) return; - emit(state.copyWith(loading: false)); + emit(state.copyWith(loading: false, fiatText: '')); } }); } @@ -99,7 +99,7 @@ class BuyConverterCubit extends Cubit { } catch (e) { developer.log(e.toString()); if (isClosed || mySeq != _seq) return; - emit(state.copyWith(loading: false)); + emit(state.copyWith(loading: false, sharesText: '')); } } diff --git a/lib/screens/sell/cubits/sell_converter/sell_converter_cubit.dart b/lib/screens/sell/cubits/sell_converter/sell_converter_cubit.dart index c9e337ce8..5106006d8 100644 --- a/lib/screens/sell/cubits/sell_converter/sell_converter_cubit.dart +++ b/lib/screens/sell/cubits/sell_converter/sell_converter_cubit.dart @@ -48,7 +48,7 @@ class SellConverterCubit extends Cubit { } catch (e) { developer.log(e.toString()); if (isClosed || mySeq != _seq) return; - emit(state.copyWith(loading: false)); + emit(state.copyWith(loading: false, sharesText: '')); } }); } @@ -75,7 +75,7 @@ class SellConverterCubit extends Cubit { } catch (e) { developer.log(e.toString()); if (isClosed || mySeq != _seq) return; - emit(state.copyWith(loading: false)); + emit(state.copyWith(loading: false, fiatText: '')); } }); } @@ -99,7 +99,7 @@ class SellConverterCubit extends Cubit { } catch (e) { developer.log(e.toString()); if (isClosed || mySeq != _seq) return; - emit(state.copyWith(loading: false)); + emit(state.copyWith(loading: false, fiatText: '')); } } diff --git a/test/screens/buy/cubits/buy_converter_cubit_test.dart b/test/screens/buy/cubits/buy_converter_cubit_test.dart index 53ecc7186..dd2fe5ab5 100644 --- a/test/screens/buy/cubits/buy_converter_cubit_test.dart +++ b/test/screens/buy/cubits/buy_converter_cubit_test.dart @@ -86,6 +86,31 @@ void main() { expect(cubit.state.loading, isFalse); }); + test('onFiatChanged clears stale sharesText when a later conversion fails', () async { + when(() => service.getBuyShares('300', any())).thenAnswer( + (_) async => BrokerbotBuySharesDto( + shares: 209, + pricePerShare: 1.43, + availableShares: 100000, + ), + ); + when(() => service.getBuyShares('1.000', any())).thenAnswer( + (_) async => throw Exception('Shares request failed: amountInput is not valid'), + ); + + final cubit = BuyConverterCubit(service); + await cubit.onFiatChanged('300'); + await Future.delayed(const Duration(milliseconds: 250)); + expect(cubit.state.sharesText, '209'); + + await cubit.onFiatChanged('1.000'); + await Future.delayed(const Duration(milliseconds: 250)); + + expect(cubit.state.fiatText, '1.000'); + expect(cubit.state.sharesText, ''); + expect(cubit.state.loading, isFalse); + }); + test( 'onSharesChanged debounces, then writes the converted fiat with matching fractional digits', () async { @@ -123,6 +148,31 @@ void main() { expect(cubit.state.loading, isFalse); }); + test('onSharesChanged clears stale fiatText when a later conversion fails', () async { + when(() => service.getBuyPrice('5', any())).thenAnswer( + (_) async => BrokerbotBuyPriceDto( + totalCost: 7.15, + pricePerShare: 1.43, + availableShares: 100, + ), + ); + when(() => service.getBuyPrice('x', any())).thenAnswer( + (_) async => throw Exception('BuyPrice request failed: sharesInput is not valid'), + ); + + final cubit = BuyConverterCubit(service); + await cubit.onSharesChanged('5'); + await Future.delayed(const Duration(milliseconds: 250)); + expect(cubit.state.fiatText, '7.15'); + + await cubit.onSharesChanged('x'); + await Future.delayed(const Duration(milliseconds: 250)); + + expect(cubit.state.sharesText, 'x'); + expect(cubit.state.fiatText, ''); + expect(cubit.state.loading, isFalse); + }); + test('onSharesChanged uses 2 fractional digits when input has no dot', () async { when(() => service.getBuyPrice(any(), any())).thenAnswer( (_) async => BrokerbotBuyPriceDto( @@ -172,6 +222,30 @@ void main() { expect(cubit.state.loading, isFalse); }); + test('onCurrencyChanged clears stale sharesText when conversion fails', () async { + when(() => service.getBuyShares(any(), Currency.chf)).thenAnswer( + (_) async => BrokerbotBuySharesDto( + shares: 209, + pricePerShare: 1.43, + availableShares: 100000, + ), + ); + when(() => service.getBuyShares(any(), Currency.eur)).thenAnswer( + (_) async => throw Exception('throttle'), + ); + + final cubit = BuyConverterCubit(service); + await cubit.onFiatChanged('300'); + await Future.delayed(const Duration(milliseconds: 250)); + expect(cubit.state.sharesText, '209'); + + await cubit.onCurrencyChanged(Currency.eur); + + expect(cubit.state.currency, Currency.eur); + expect(cubit.state.sharesText, ''); + expect(cubit.state.loading, isFalse); + }); + test( 'fiat race: stale in-flight response is dropped when user typed further', () async { diff --git a/test/screens/sell/cubits/sell_converter_cubit_test.dart b/test/screens/sell/cubits/sell_converter_cubit_test.dart index 68c64b3a7..689c469f7 100644 --- a/test/screens/sell/cubits/sell_converter_cubit_test.dart +++ b/test/screens/sell/cubits/sell_converter_cubit_test.dart @@ -104,6 +104,32 @@ void main() { expect(cubit.state.loading, isFalse); }); + test('onFiatChanged clears stale sharesText when a later conversion fails', () async { + when(() => service.getSellShares('300', any())).thenAnswer( + (_) async => BrokerbotSellSharesDto( + targetAmount: 300, + shares: 209, + pricePerShare: 1.43, + currency: 'CHF', + ), + ); + when(() => service.getSellShares('1.000', any())).thenAnswer( + (_) async => throw Exception('Shares request failed: amountInput is not valid'), + ); + + final cubit = SellConverterCubit(service); + await cubit.onFiatChanged('300'); + await Future.delayed(const Duration(milliseconds: 250)); + expect(cubit.state.sharesText, '209'); + + await cubit.onFiatChanged('1.000'); + await Future.delayed(const Duration(milliseconds: 250)); + + expect(cubit.state.fiatText, '1.000'); + expect(cubit.state.sharesText, ''); + expect(cubit.state.loading, isFalse); + }); + test('onSharesChanged writes estimatedAmount with matching fractional digits', () async { when(() => service.getSellPrice(any(), any())).thenAnswer( (_) async => BrokerbotSellPriceDto( @@ -140,6 +166,32 @@ void main() { expect(cubit.state.loading, isFalse); }); + test('onSharesChanged clears stale fiatText when a later conversion fails', () async { + when(() => service.getSellPrice('5', any())).thenAnswer( + (_) async => BrokerbotSellPriceDto( + shares: 5, + estimatedAmount: 7.15, + pricePerShare: 1.43, + currency: 'CHF', + ), + ); + when(() => service.getSellPrice('x', any())).thenAnswer( + (_) async => throw Exception('SellPrice request failed: sharesInput is not valid'), + ); + + final cubit = SellConverterCubit(service); + await cubit.onSharesChanged('5'); + await Future.delayed(const Duration(milliseconds: 250)); + expect(cubit.state.fiatText, '7.15'); + + await cubit.onSharesChanged('x'); + await Future.delayed(const Duration(milliseconds: 250)); + + expect(cubit.state.sharesText, 'x'); + expect(cubit.state.fiatText, ''); + expect(cubit.state.loading, isFalse); + }); + test('onSharesChanged defaults to 2 fractional digits when input has no dot', () async { when(() => service.getSellPrice(any(), any())).thenAnswer( (_) async => BrokerbotSellPriceDto( @@ -213,6 +265,31 @@ void main() { expect(cubit.state.loading, isFalse); }); + test('onCurrencyChanged clears stale fiatText when conversion fails', () async { + when(() => service.getSellPrice(any(), Currency.chf)).thenAnswer( + (_) async => BrokerbotSellPriceDto( + shares: 10, + estimatedAmount: 14.30, + pricePerShare: 1.43, + currency: 'CHF', + ), + ); + when(() => service.getSellPrice(any(), Currency.eur)).thenAnswer( + (_) async => throw Exception('throttle'), + ); + + final cubit = SellConverterCubit(service); + await cubit.onSharesChanged('10'); + await Future.delayed(const Duration(milliseconds: 250)); + expect(cubit.state.fiatText, '14.30'); + + await cubit.onCurrencyChanged(Currency.eur); + + expect(cubit.state.currency, Currency.eur); + expect(cubit.state.fiatText, ''); + expect(cubit.state.loading, isFalse); + }); + test( 'fiat race: stale in-flight response is dropped when user typed further', () async { From 1df5eb7feb903f4ce08822fb01e8300984eeebca Mon Sep 17 00:00:00 2001 From: joshuakrueger-dfx Date: Wed, 19 Aug 2026 17:14:41 +0200 Subject: [PATCH 03/18] feat(buy,sell): resolve a thousands separator in the amount field instead of refusing it The app only offers EUR and CHF, both two-decimal currencies, so a separator followed by exactly three digits cannot be a decimal place: 1.000 and 1,000 both mean one thousand. The field now resolves that pattern and shows the result, so the user sees 1000 before committing to a purchase rather than being told to retype it. Partial input is left alone while typing (1, 1., 1.0, 1.00) and the caret stays at the end, so entering 1.000.000 keeps working character by character. The parser-level rejection stays in place as a safety net for values that do not come through the field, such as a deeplink or a paste. --- lib/packages/utils/fiat_amount.dart | 10 ++ .../buy/widgets/payment_converter.dart | 3 +- lib/screens/sell/widgets/sell_converter.dart | 3 +- lib/widgets/fiat_input_formatter.dart | 21 ++++ test/packages/utils/fiat_amount_test.dart | 23 ++++ .../buy/buy_amount_change_repro_test.dart | 100 +++++++++++++----- .../widgets/sell_converter_error_test.dart | 56 ++++++++++ 7 files changed, 190 insertions(+), 26 deletions(-) create mode 100644 lib/widgets/fiat_input_formatter.dart diff --git a/lib/packages/utils/fiat_amount.dart b/lib/packages/utils/fiat_amount.dart index cf732a473..b245f5b48 100644 --- a/lib/packages/utils/fiat_amount.dart +++ b/lib/packages/utils/fiat_amount.dart @@ -7,6 +7,16 @@ double? tryParseFiatAmount(String input) { return double.tryParse(input.replaceAll(',', '.')); } +/// Strips a lone thousands group (`1.000` / `1,000` → `1000`). +/// +/// EUR and CHF have two decimal places, so a separator followed by exactly +/// three digits cannot be a decimal. Every other input is returned unchanged, +/// including partials (`1.`, `1.0`, `1.00`) and real decimals (`300,75`). +String normalizeFiatInput(String input) { + if (!RegExp(r'^\d+[.,]\d{3}$').hasMatch(input)) return input; + return input.replaceAll('.', '').replaceAll(',', ''); +} + /// The whole-currency integer the backend charges for the raw [input] the user /// typed (e.g. `300,75` → `301`); empty input counts as zero. int chargedFiatAmount(String input) { diff --git a/lib/screens/buy/widgets/payment_converter.dart b/lib/screens/buy/widgets/payment_converter.dart index 4f9d0f9c3..b0ba7279e 100644 --- a/lib/screens/buy/widgets/payment_converter.dart +++ b/lib/screens/buy/widgets/payment_converter.dart @@ -11,6 +11,7 @@ import 'package:realunit_wallet/screens/buy/cubits/buy_converter/buy_converter_c import 'package:realunit_wallet/setup/di.dart'; import 'package:realunit_wallet/styles/colors.dart'; import 'package:realunit_wallet/styles/currency.dart'; +import 'package:realunit_wallet/widgets/fiat_input_formatter.dart'; class PaymentConverter extends StatefulWidget { const PaymentConverter({ @@ -100,7 +101,7 @@ class _PaymentConverterState extends State { child: TextField( controller: _amountController, keyboardType: const .numberWithOptions(decimal: true), - inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9.,]'))], + inputFormatters: const [FiatInputFormatter()], decoration: const InputDecoration( border: .none, contentPadding: .symmetric( diff --git a/lib/screens/sell/widgets/sell_converter.dart b/lib/screens/sell/widgets/sell_converter.dart index 8d4e064c4..b25e7862f 100644 --- a/lib/screens/sell/widgets/sell_converter.dart +++ b/lib/screens/sell/widgets/sell_converter.dart @@ -14,6 +14,7 @@ import 'package:realunit_wallet/screens/sell/widgets/sell_max_amount_button.dart import 'package:realunit_wallet/setup/di.dart'; import 'package:realunit_wallet/styles/colors.dart'; import 'package:realunit_wallet/styles/currency.dart'; +import 'package:realunit_wallet/widgets/fiat_input_formatter.dart'; class SellConverter extends StatefulWidget { const SellConverter({ @@ -222,7 +223,7 @@ class _SellConverterState extends State { child: TextField( controller: _resultController, keyboardType: const TextInputType.numberWithOptions(decimal: true), - inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9.,]'))], + inputFormatters: const [FiatInputFormatter()], decoration: const InputDecoration( border: InputBorder.none, contentPadding: EdgeInsets.symmetric( diff --git a/lib/widgets/fiat_input_formatter.dart b/lib/widgets/fiat_input_formatter.dart new file mode 100644 index 000000000..9e8d15691 --- /dev/null +++ b/lib/widgets/fiat_input_formatter.dart @@ -0,0 +1,21 @@ +import 'package:flutter/services.dart'; +import 'package:realunit_wallet/packages/utils/fiat_amount.dart'; + +/// Digits and separators only. A completed thousands group (`1.000` / `1,000`) +/// is rewritten to the integer; partials (`1.`, `1.0`, `1.00`) stay as typed. +class FiatInputFormatter extends TextInputFormatter { + const FiatInputFormatter(); + + static final _allowedChars = FilteringTextInputFormatter.allow(RegExp(r'[0-9.,]')); + + @override + TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { + final allowed = _allowedChars.formatEditUpdate(oldValue, newValue); + final normalized = normalizeFiatInput(allowed.text); + if (normalized == allowed.text) return allowed; + return TextEditingValue( + text: normalized, + selection: TextSelection.collapsed(offset: normalized.length), + ); + } +} diff --git a/test/packages/utils/fiat_amount_test.dart b/test/packages/utils/fiat_amount_test.dart index dcc2215f8..5661c1c07 100644 --- a/test/packages/utils/fiat_amount_test.dart +++ b/test/packages/utils/fiat_amount_test.dart @@ -59,4 +59,27 @@ void main() { expect(tryParseFiatAmount('1,50'), 1.5); }); }); + + group('normalizeFiatInput', () { + test('strips a lone thousands group', () { + expect(normalizeFiatInput('1.000'), '1000'); + expect(normalizeFiatInput('1,000'), '1000'); + expect(normalizeFiatInput('10.000'), '10000'); + expect(normalizeFiatInput('10,000'), '10000'); + }); + + test('leaves every other input unchanged', () { + expect(normalizeFiatInput('300'), '300'); + expect(normalizeFiatInput('300,75'), '300,75'); + expect(normalizeFiatInput('0,5'), '0,5'); + expect(normalizeFiatInput('1.300,75'), '1.300,75'); + expect(normalizeFiatInput('3,5,7'), '3,5,7'); + }); + + test('leaves partial thousands-group input unchanged', () { + expect(normalizeFiatInput('1.'), '1.'); + expect(normalizeFiatInput('1.0'), '1.0'); + expect(normalizeFiatInput('1.00'), '1.00'); + }); + }); } diff --git a/test/screens/buy/buy_amount_change_repro_test.dart b/test/screens/buy/buy_amount_change_repro_test.dart index ace565860..ea6f7ba20 100644 --- a/test/screens/buy/buy_amount_change_repro_test.dart +++ b/test/screens/buy/buy_amount_change_repro_test.dart @@ -120,52 +120,104 @@ void main() { ); testWidgets( - 'typing 1.000 (thousands grouping) does not show the support-contact error', + 'typing 1.000 (thousands grouping) is shown as 1000 and does not show an error', (tester) async { await _pumpLoadedBuyPage(tester); await _enterAmount(tester, '1.000'); final snapshot = _snapshot(tester); - final amount = tester.widget(_amountField); - expect( find.text(S.current.invalidAmountFormatTitle), - findsOneWidget, + findsNothing, reason: - 'Typing 1.000 (grouping-ambiguous) must show the specific ' - 'ambiguous-amount hint. $snapshot', + 'Typing 1.000 must be rewritten to 1000; the ambiguous-amount ' + 'hint must not appear. $snapshot', ); expect( find.text(S.current.invalidAmountFormatDescription), - findsOneWidget, + findsNothing, reason: - 'Typing 1.000 (grouping-ambiguous) must show the specific ' - 'ambiguous-amount description. $snapshot', + 'Typing 1.000 must be rewritten to 1000; the ambiguous-amount ' + 'description must not appear. $snapshot', + ); + _expectHealthyQuote( + tester, + expectedAmount: '1000', + reasonPrefix: + 'Typing 1.000 must appear in the field as 1000 and keep a valid ' + 'quote. The field normalises the thousands group before the ' + 'parser sees it.', ); + }, + ); + + testWidgets( + 'typing 1.000 character by character leaves partials alone, then shows 1000', + (tester) async { + await _pumpLoadedBuyPage(tester); + await tester.enterText(_amountField, ''); + await tester.pump(); + + for (final value in ['1', '1.', '1.0', '1.00']) { + await tester.enterText(_amountField, value); + await tester.pump(); + expect( + tester.widget(_amountField).controller!.text, + value, + reason: + 'Intermediate "$value" must stay as typed; the formatter must ' + 'not rewrite partial thousands-group input. ${_snapshot(tester)}', + ); + } + + await tester.enterText(_amountField, '1.000'); + await tester.pump(); + + final afterGroup = tester.widget(_amountField).controller!; expect( - find.text(S.current.paymentInformationFailed), - findsNothing, + afterGroup.text, + '1000', reason: - 'Typing 1.000 (grouping-ambiguous) must not become the generic ' - 'support-contact error. $snapshot', + 'The last character of 1.000 must rewrite the field to 1000. ' + '${_snapshot(tester)}', ); expect( - find.text(S.current.paymentInformationFailedDescription), - findsNothing, + afterGroup.selection, + const TextSelection.collapsed(offset: 4), reason: - 'Typing 1.000 (grouping-ambiguous) must not show the support ' - 'description. $snapshot', + 'After rewriting 1.000 to 1000 the caret must sit at the end ' + 'so further typing appends. ${_snapshot(tester)}', ); - expect( - find.byType(CupertinoActivityIndicator), - findsNothing, - reason: 'Payment info is still spinning. $snapshot', + + for (final value in ['1000.', '1000.0', '1000.00']) { + await tester.enterText(_amountField, value); + await tester.pump(); + expect( + tester.widget(_amountField).controller!.text, + value, + reason: + 'Continuation "$value" must stay as typed. ${_snapshot(tester)}', + ); + } + + await tester.enterText(_amountField, '1000.000'); + await tester.pump(_afterDebounce); + await tester.pump(); + + _expectHealthyQuote( + tester, + expectedAmount: '1000000', + reasonPrefix: + 'Typing .000 after 1000 must appear in the field as 1000000 ' + 'and keep a valid quote.', ); expect( - amount.controller!.text, - '1.000', - reason: 'Amount field is not "1.000". $snapshot', + tester.widget(_amountField).controller!.selection, + const TextSelection.collapsed(offset: 7), + reason: + 'After rewriting 1000.000 to 1000000 the caret must sit at ' + 'the end. ${_snapshot(tester)}', ); }, ); diff --git a/test/screens/sell/widgets/sell_converter_error_test.dart b/test/screens/sell/widgets/sell_converter_error_test.dart index 270666e6d..1623e0827 100644 --- a/test/screens/sell/widgets/sell_converter_error_test.dart +++ b/test/screens/sell/widgets/sell_converter_error_test.dart @@ -95,4 +95,60 @@ void main() { expect(find.byType(SnackBar), findsNothing); }, ); + + testWidgets( + 'typing 1.000 in the you-receive field is shown as 1000', + (tester) async { + when(() => fiatRepo.getSellable()).thenAnswer((_) async => const [Currency.chf]); + when(() => converterCubit.onFiatChanged(any())).thenAnswer((_) async {}); + + final sharesController = TextEditingController(); + final fiatController = TextEditingController(); + addTearDown(sharesController.dispose); + addTearDown(fiatController.dispose); + + await tester.pumpApp( + MultiBlocProvider( + providers: [ + BlocProvider.value(value: converterCubit), + BlocProvider.value(value: balanceCubit), + ], + child: Scaffold( + body: SellConverter( + amountController: sharesController, + resultController: fiatController, + ), + ), + ), + ); + await tester.pump(); + + final fiatField = find.byWidgetPredicate( + (widget) => widget is TextField && widget.controller == fiatController, + ); + + for (final value in ['1', '1.', '1.0', '1.00']) { + await tester.enterText(fiatField, value); + await tester.pump(); + expect( + fiatController.text, + value, + reason: 'Intermediate "$value" must stay as typed in the sell fiat field.', + ); + } + + await tester.enterText(fiatField, '1.000'); + await tester.pump(); + + expect(fiatController.text, '1000'); + expect(fiatController.selection, const TextSelection.collapsed(offset: 4)); + expect(sharesController.text, isEmpty); + + await tester.enterText(fiatField, '1000.000'); + await tester.pump(); + + expect(fiatController.text, '1000000'); + expect(fiatController.selection, const TextSelection.collapsed(offset: 7)); + }, + ); } From d0eaaece1d078745c8f7159c17f61d8828c657cb Mon Sep 17 00:00:00 2001 From: joshuakrueger-dfx Date: Wed, 19 Aug 2026 17:32:55 +0200 Subject: [PATCH 04/18] test(goldens): cover the ambiguous-amount error state Every other failure state of the buy screen has a golden. This one did not, so nothing guarded how the new panel renders. The state is driven with 1.300,75 rather than 1.000: the field formatter rewrites a lone thousands group before the parser sees it, so only a mixed grouping-and-decimal value still reaches chargedFiatAmount and throws. --- test/goldens/screens/buy/buy_golden_test.dart | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/goldens/screens/buy/buy_golden_test.dart b/test/goldens/screens/buy/buy_golden_test.dart index 25791c972..664056428 100644 --- a/test/goldens/screens/buy/buy_golden_test.dart +++ b/test/goldens/screens/buy/buy_golden_test.dart @@ -270,6 +270,28 @@ void main() { }, ); + goldenTest( + 'invalid amount format failure', + fileName: 'buy_invalid_amount_format', + constraints: const BoxConstraints.tightFor(width: 390, height: 844), + builder: () { + when(() => paymentInfoCubit.state).thenReturn( + const BuyPaymentInfoFailure(PaymentInfoError.invalidAmountFormat), + ); + when(() => converterCubit.state).thenReturn( + const BuyConverterState( + // Mixed grouping+decimal. The field formatter only rewrites a + // lone thousands group (`1.000` → `1000`); this value reaches + // chargedFiatAmount, which throws FormatException. + fiatText: '1.300,75', + sharesText: '', + currency: Currency.chf, + ), + ); + return wrapForGolden(buildSubject()); + }, + ); + goldenTest( 'price source unavailable failure shows API message', fileName: 'buy_price_source_unavailable', From b1b0fd8e80d42eae07d1e04ba826beb86dcfe755 Mon Sep 17 00:00:00 2001 From: joshuakrueger-dfx Date: Wed, 19 Aug 2026 17:41:20 +0200 Subject: [PATCH 05/18] test(goldens): add the ambiguous-amount baseline Rendered by the golden-regenerate workflow on the self-hosted runner, so it matches the toolchain that validates it on PRs. Committed here signed rather than pushed by the workflow bot, which commits unsigned. --- .../goldens/macos/buy_invalid_amount_format.png | Bin 0 -> 26628 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/goldens/screens/buy/goldens/macos/buy_invalid_amount_format.png diff --git a/test/goldens/screens/buy/goldens/macos/buy_invalid_amount_format.png b/test/goldens/screens/buy/goldens/macos/buy_invalid_amount_format.png new file mode 100644 index 0000000000000000000000000000000000000000..538fd3898bf2d6a1c1936e566373c5f7c7e7ac06 GIT binary patch literal 26628 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
;NoCJmvRawD@(n5`#CbXf zbNcj$+T@7(k@f6zpMnlF7p2f>@)v!|!d{4Nx3}ZzxlU2Y|^wg zrDT8<5;zYsYM8f&&ZZQA{VX&d_ZBBtwZStf5gEDT-YDJ9%CbG%iIx5g`&HJ!buU%N zeqept(?IcM_V#!*Cyt{j<4#Q$WBRwIgM^no)`3B2Y(b>zne3}OJ2G`VK<*Wk*%F80 z?W7vu{_*!}=%u(gl)7#bwzN6hR~#x0cIFOYA|@90sN*5zSO~ti zt*wo_kR_X$o^7A!A5!+{j!>8;=!xwdCFq6N(hRAL`YUjz-KMqQQ28~0vDxb{b>CU~ zPz}QKPuXCh;1|W243b|tkP(2&TV9`T6Puc(f~^}u|0oeds>?ib^oZ31Ul35BKz@)h zUqaMfR#5)k&=?@6^Y~B9&0*#ejc8nE6}UmxLCye<{OypE66wh} z9fckJC|55{IFLv`cHE}$MnXFT?lm0@JV}pZ%#C3H;G4*~`T=Ctp!nvcn=P~l;%n9g zZ{9+eniVvu&j$&6@Cl&OJX<%9`!W`9MEULqdPSb^-`{tMycAXws(V{agZG#>@zWd~ zZm5H(?94PEYQu0)Fu%d$@mhHmNe6XMb$3sAv7bEejE5xt09M@9`DWfiDgoLTjsQfH zanitzuh+Q)2!wk0I=OBG9g;8dI6EMK?@{JWcVW6|mF(sl9mB(VZPG&z?(k9|=g-&iwMCu#mbS8l%cAtafB7)8FB<9I!|D&v z$%05w!-JjO_o`LL;2?&e)A`##ANiGpgv;?g5)$KSyRU$&1*q>$ci&e>$vc7;1enh+ z`?B@6zjNIG{v>v59f$^_-%|1#j-Z;;L!xbFa_$G+k?+%1by7L~p;(-6V5Ag?&g@?l zC3i}cq;@&&L)y0I}LJJCv9BT9EVwFqFSPx+ZFivXGR}nN9l|pJT}x zcFbS$>l}lp{tT9PEKOr{%tOl#PMo=kUR$^gJdRQFzy(ZDMMcG=%c%bH4H0Zbx$}9i zO@Xn@*$8ZHqeH&&CU)(~t^$_;BQNLm!oos#1Cc~v(T*$fI_ITSaP<+K?P<9sjhWi> zT0f6THPC*$ed9PJ-&kgG)kfRmqF1;N%uJ$z5q<7Mf`*jJwbl!T=Eh066JondwP}ls zL#WP%dB9%w0r3yq92t$;eylej5eTn5Wgy{h_Sm=w6yT7cMSclpN(HZrnky~^Exik) zgR%#Iz3(mtCQ>;_k7Z^7|DO@W0s>2af@{vyQH5>Z1Rh z%SAm;b0qfyY8dzcin?{i1)cNRj+~}H?eh-IGMOiSlG*5V_ByD}?I$`CQ~&uoS4I^Q z3Cb@z5%VnUQ?zyseXw~Ao@MhqKHdg2Vl6=x*@8m7GuiZOykWvDUQVJsmH~CJf?4R@ zIsujfoVTJ|f17aX-(UdOUgpfrg<|=IEfRMI{Uk2CMl?%EoZGQgqTwj;u}9}5B=&s# zcRX+z`V%mdB%g`@BjDd-^?#QBOWf Date: Thu, 20 Aug 2026 09:40:05 +0200 Subject: [PATCH 06/18] test(buy): pin that a changed amount is the one that gets confirmed The customer reported he could only buy at the pre-filled 300. Production shows valid quotes at 100 and 200 that he never completed, which does not distinguish would-not from could-not. Drive the purchase through BuyPage after replacing the amount, and after a currency switch, asserting the confirmed quote is the current one. The quote id encodes amount and currency, so a leftover 300 fails the assertion instead of passing silently. --- .../buy/buy_amount_change_repro_test.dart | 250 +++++++++++++++++- 1 file changed, 248 insertions(+), 2 deletions(-) diff --git a/test/screens/buy/buy_amount_change_repro_test.dart b/test/screens/buy/buy_amount_change_repro_test.dart index ea6f7ba20..660db4ec7 100644 --- a/test/screens/buy/buy_amount_change_repro_test.dart +++ b/test/screens/buy/buy_amount_change_repro_test.dart @@ -20,8 +20,10 @@ import 'package:realunit_wallet/packages/utils/fiat_amount.dart'; import 'package:realunit_wallet/screens/buy/buy_page.dart'; import 'package:realunit_wallet/screens/buy/cubits/buy_converter/buy_converter_cubit.dart'; import 'package:realunit_wallet/screens/buy/cubits/buy_payment_info/buy_payment_info_cubit.dart'; +import 'package:realunit_wallet/screens/buy/widgets/buy_confirm_button.dart'; import 'package:realunit_wallet/screens/buy/widgets/payment_action_required.dart'; import 'package:realunit_wallet/styles/currency.dart'; +import 'package:realunit_wallet/widgets/buttons/app_filled_button.dart'; import '../../helper/helper.dart'; @@ -221,6 +223,135 @@ void main() { ); }, ); + + testWidgets( + 'buying after replacing 300 with 100 confirms the 100 quote, not the leftover 300', + (tester) async { + await _pumpLoadedBuyPage(tester); + + _expectHealthyQuote( + tester, + expectedAmount: '300', + reasonPrefix: 'Default 300 must be a valid quote before the amount change.', + ); + expect( + find.byType(BuyConfirmButton), + findsOneWidget, + reason: + 'Default 300 must already show the binding-buy button. ' + '${_snapshot(tester)}', + ); + + await _enterAmount(tester, ''); + await _enterAmount(tester, '100'); + + _expectHealthyQuote( + tester, + expectedAmount: '100', + reasonPrefix: + 'After replacing 300 with 100 the screen must keep a valid quote.', + ); + + await _tapConfirmAndVerifyQuote( + tester, + paymentInfoService, + amount: 100, + currency: Currency.chf, + leftoverQuoteIds: [_quoteId(300, Currency.chf)], + reasonPrefix: + 'A buy after changing 300 to 100 must confirm the 100 quote, ' + 'never the leftover default 300.', + ); + }, + ); + + testWidgets( + 'buying after CHF to EUR then replacing 300 with 100 confirms the 100 EUR quote, ' + 'not a leftover 300', + (tester) async { + await _pumpLoadedBuyPage(tester); + + await _selectBuyCurrency(tester, Currency.eur); + + _expectHealthyQuote( + tester, + expectedAmount: '300', + reasonPrefix: 'After switching to EUR at 300 the quote must stay valid.', + ); + expect( + tester.element(find.byType(BuyView)).read().state.currency, + Currency.eur, + reason: + 'Currency picker must have flipped the converter to EUR. ' + '${_snapshot(tester)}', + ); + final eurQuote = tester + .element(find.byType(BuyView)) + .read() + .state; + expect( + eurQuote, + isA(), + reason: + '300 EUR must have landed as a valid quote before the amount ' + 'change. ${_snapshot(tester)}', + ); + final eurSuccess = eurQuote as BuyPaymentInfoSuccess; + expect( + eurSuccess.buyPaymentInfo.currency, + Currency.eur, + reason: + 'The landed quote must be EUR, not a leftover CHF quote. ' + '${_snapshot(tester)}', + ); + expect( + eurSuccess.buyPaymentInfo.amount, + 300.0, + reason: + 'The EUR quote before the amount change must still be 300. ' + '${_snapshot(tester)}', + ); + expect( + find.byType(BuyConfirmButton), + findsOneWidget, + reason: + '300 EUR must show the binding-buy button before the amount ' + 'change. ${_snapshot(tester)}', + ); + + await _enterAmount(tester, ''); + await _enterAmount(tester, '100'); + + _expectHealthyQuote( + tester, + expectedAmount: '100', + reasonPrefix: + 'After switching to EUR and replacing 300 with 100 the quote ' + 'must stay valid.', + ); + expect( + tester.element(find.byType(BuyView)).read().state.currency, + Currency.eur, + reason: + 'Currency must stay EUR after the amount change. ' + '${_snapshot(tester)}', + ); + + await _tapConfirmAndVerifyQuote( + tester, + paymentInfoService, + amount: 100, + currency: Currency.eur, + leftoverQuoteIds: [ + _quoteId(300, Currency.chf), + _quoteId(300, Currency.eur), + ], + reasonPrefix: + 'A buy after CHF to EUR and 300 to 100 must confirm the 100 EUR ' + 'quote, never a leftover 300 CHF or 300 EUR quote.', + ); + }, + ); }); } @@ -284,7 +415,7 @@ BuyPaymentInfo _quote({ double? minVolume, }) { return BuyPaymentInfo( - id: 1, + id: _quoteId(amount, currency), iban: 'CH56 0483 5012 3456 78', bic: 'CRESCHZZ80A', name: 'DFX AG', @@ -301,6 +432,15 @@ BuyPaymentInfo _quote({ ); } +/// Distinct per (amount, currency) so a leftover default quote cannot +/// masquerade as the edited one when [confirmPayment] is verified by id. +int _quoteId(int amount, Currency currency) { + return switch (currency) { + Currency.chf => amount, + Currency.eur => 1000000 + amount, + }; +} + Finder get _amountField => find.byType(TextField).first; Future _pumpLoadedBuyPage(WidgetTester tester) async { @@ -331,6 +471,110 @@ Future _enterAmount(WidgetTester tester, String value) async { await tester.pump(); } +Future _selectBuyCurrency(WidgetTester tester, Currency currency) async { + await tester.tap(find.byKey(const Key('buy-currency-picker'))); + await tester.pumpAndSettle(); + + await tester.tap( + find.byWidgetPredicate( + (widget) => widget is PopupMenuItem && widget.value == currency, + ), + ); + await tester.pumpAndSettle(); +} + +/// Taps the binding-buy CTA and asserts the confirm call carries the current +/// quote, not a leftover one. Confirm is stubbed to throw so the page does +/// not try to `pushNamed` (this file hosts [BuyPage] via [pumpApp], not a +/// GoRouter). The call itself is what the production path charges. +Future _tapConfirmAndVerifyQuote( + WidgetTester tester, + MockRealUnitBuyPaymentInfoService paymentInfo, { + required int amount, + required Currency currency, + required List leftoverQuoteIds, + required String reasonPrefix, +}) async { + final snapshot = _snapshot(tester); + final currentId = _quoteId(amount, currency); + + expect( + find.byType(BuyConfirmButton), + findsOneWidget, + reason: '$reasonPrefix Binding-buy button is missing. $snapshot', + ); + + final confirm = tester.widget(find.byType(BuyConfirmButton)); + expect( + confirm.buyPaymentInfo.amount, + amount.toDouble(), + reason: + '$reasonPrefix Confirm button still holds amount ' + '${confirm.buyPaymentInfo.amount}, not $amount. $snapshot', + ); + expect( + confirm.buyPaymentInfo.currency, + currency, + reason: + '$reasonPrefix Confirm button still holds currency ' + '${confirm.buyPaymentInfo.currency.code}, not ${currency.code}. $snapshot', + ); + expect( + confirm.buyPaymentInfo.id, + currentId, + reason: + '$reasonPrefix Confirm button still holds quote id ' + '${confirm.buyPaymentInfo.id}, not the current quote $currentId. $snapshot', + ); + expect( + leftoverQuoteIds.contains(confirm.buyPaymentInfo.id), + isFalse, + reason: + '$reasonPrefix Confirm button still holds a leftover quote id ' + '${confirm.buyPaymentInfo.id}. $snapshot', + ); + + final filled = tester.widget( + find.descendant( + of: find.byType(BuyConfirmButton), + matching: find.byType(AppFilledButton), + ), + ); + expect( + filled.onPressed, + isNotNull, + reason: '$reasonPrefix Binding-buy button is not pressable. $snapshot', + ); + + when(() => paymentInfo.confirmPayment(any())).thenAnswer( + (_) async => throw Exception('confirm outcome is not under test'), + ); + + await tester.tap(find.text(S.current.buyPaymentConfirm)); + await tester.pump(); + await tester.pump(); + + final confirmedIds = + verify(() => paymentInfo.confirmPayment(captureAny())).captured.cast(); + expect( + confirmedIds, + [currentId], + reason: + '$reasonPrefix Confirm must be called with the current quote id ' + '$currentId (amount $amount ${currency.code}), not a leftover ' + 'quote. Got $confirmedIds. ${_snapshot(tester)}', + ); + for (final leftoverId in leftoverQuoteIds) { + expect( + confirmedIds, + isNot(contains(leftoverId)), + reason: + '$reasonPrefix Confirm carried leftover quote id $leftoverId. ' + 'Got $confirmedIds. ${_snapshot(tester)}', + ); + } +} + void _expectHealthyQuote( WidgetTester tester, { required String expectedAmount, @@ -379,7 +623,9 @@ String _snapshot(WidgetTester tester) { String _describePayment(BuyPaymentInfoState state) { if (state is BuyPaymentInfoSuccess) { - return 'Success(amount=${state.buyPaymentInfo.amount}, ' + return 'Success(id=${state.buyPaymentInfo.id}, ' + 'amount=${state.buyPaymentInfo.amount}, ' + 'currency=${state.buyPaymentInfo.currency.code}, ' 'isValid=${state.buyPaymentInfo.isValid})'; } if (state is BuyPaymentInfoMinAmountNotMetFailure) { From 6a38394bd2b7ca8764aeabbb03147d9e3165ff68 Mon Sep 17 00:00:00 2001 From: joshuakrueger-dfx Date: Thu, 20 Aug 2026 10:35:56 +0200 Subject: [PATCH 07/18] fix(l10n): describe what actually triggers the unreadable-amount notice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The field now resolves a lone thousands group, so 1.000 no longer reaches the parser from the keyboard. What still reaches it are values with more than one separator, such as 1.300,75 — for those, telling the user to drop the thousands separator describes the wrong problem. Name the condition that is left: at most one separator. --- assets/languages/strings_de.arb | 4 ++-- assets/languages/strings_en.arb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/languages/strings_de.arb b/assets/languages/strings_de.arb index 208535220..074bf8a85 100644 --- a/assets/languages/strings_de.arb +++ b/assets/languages/strings_de.arb @@ -111,8 +111,8 @@ "identityCheckProcessDescription": "Als nächstes müssen Sie Ihre Identität verifizieren. Bitte halten Sie Ihren Ausweis bereit und erlauben Sie den Kamerazugriff auf dem Gerät.", "identityCheckRequired": "Identitätsprüfung erforderlich", "imprint": "Impressum", - "invalidAmountFormatDescription": "Bitte geben Sie den Betrag ohne Tausenderpunkt ein, zum Beispiel 1000 statt 1.000.", - "invalidAmountFormatTitle": "Betrag nicht eindeutig", + "invalidAmountFormatDescription": "Bitte geben Sie den Betrag mit höchstens einem Trennzeichen ein, zum Beispiel 1000 oder 1000,50.", + "invalidAmountFormatTitle": "Betrag nicht lesbar", "kyc": "Eröffnungsprozess", "kycAccountMergeDescription": "Ihre Identität wurde bereits in einem anderen Konto gefunden. Eine Zusammenführungsanfrage wurde erstellt. Bitte bestätigen Sie diese über die E-Mail, die Sie erhalten haben.", "kycAccountMergeTitle": "Kontozusammenführung erforderlich", diff --git a/assets/languages/strings_en.arb b/assets/languages/strings_en.arb index 4c3ad8105..b869c0b13 100644 --- a/assets/languages/strings_en.arb +++ b/assets/languages/strings_en.arb @@ -111,8 +111,8 @@ "identityCheckProcessDescription": "Next, you need to verify your identity. Please have your ID ready and allow camera access on your device.", "identityCheckRequired": "Identity check required", "imprint": "Imprint", - "invalidAmountFormatDescription": "Please enter the amount without a thousands separator, for example 1000 instead of 1.000.", - "invalidAmountFormatTitle": "Amount is ambiguous", + "invalidAmountFormatDescription": "Please enter the amount with at most one separator, for example 1000 or 1000.50.", + "invalidAmountFormatTitle": "Amount is not readable", "kyc": "Onboarding process", "kycAccountMergeDescription": "Your identity was found in another account. A merge request has been created. Please confirm it via the email you received.", "kycAccountMergeTitle": "Account merge required", From 0fa57755984502528140b726b1e449a5926edc67 Mon Sep 17 00:00:00 2001 From: joshuakrueger-dfx Date: Thu, 20 Aug 2026 10:40:35 +0200 Subject: [PATCH 08/18] test(goldens): refresh the unreadable-amount baseline for the new wording Rendered by golden-regenerate on the self-hosted runner, committed here signed rather than pushed by the workflow bot. --- .../macos/buy_invalid_amount_format.png | Bin 26628 -> 26533 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/goldens/screens/buy/goldens/macos/buy_invalid_amount_format.png b/test/goldens/screens/buy/goldens/macos/buy_invalid_amount_format.png index 538fd3898bf2d6a1c1936e566373c5f7c7e7ac06..4f23578ffcf06fa5bfeb1657b1f12b616681667f 100644 GIT binary patch literal 26533 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 OkfoX3<+6)6{`?<0(e@|+ literal 26628 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
;NoCJmvRawD@(n5`#CbXf zbNcj$+T@7(k@f6zpMnlF7p2f>@)v!|!d{4Nx3}ZzxlU2Y|^wg zrDT8<5;zYsYM8f&&ZZQA{VX&d_ZBBtwZStf5gEDT-YDJ9%CbG%iIx5g`&HJ!buU%N zeqept(?IcM_V#!*Cyt{j<4#Q$WBRwIgM^no)`3B2Y(b>zne3}OJ2G`VK<*Wk*%F80 z?W7vu{_*!}=%u(gl)7#bwzN6hR~#x0cIFOYA|@90sN*5zSO~ti zt*wo_kR_X$o^7A!A5!+{j!>8;=!xwdCFq6N(hRAL`YUjz-KMqQQ28~0vDxb{b>CU~ zPz}QKPuXCh;1|W243b|tkP(2&TV9`T6Puc(f~^}u|0oeds>?ib^oZ31Ul35BKz@)h zUqaMfR#5)k&=?@6^Y~B9&0*#ejc8nE6}UmxLCye<{OypE66wh} z9fckJC|55{IFLv`cHE}$MnXFT?lm0@JV}pZ%#C3H;G4*~`T=Ctp!nvcn=P~l;%n9g zZ{9+eniVvu&j$&6@Cl&OJX<%9`!W`9MEULqdPSb^-`{tMycAXws(V{agZG#>@zWd~ zZm5H(?94PEYQu0)Fu%d$@mhHmNe6XMb$3sAv7bEejE5xt09M@9`DWfiDgoLTjsQfH zanitzuh+Q)2!wk0I=OBG9g;8dI6EMK?@{JWcVW6|mF(sl9mB(VZPG&z?(k9|=g-&iwMCu#mbS8l%cAtafB7)8FB<9I!|D&v z$%05w!-JjO_o`LL;2?&e)A`##ANiGpgv;?g5)$KSyRU$&1*q>$ci&e>$vc7;1enh+ z`?B@6zjNIG{v>v59f$^_-%|1#j-Z;;L!xbFa_$G+k?+%1by7L~p;(-6V5Ag?&g@?l zC3i}cq;@&&L)y0I}LJJCv9BT9EVwFqFSPx+ZFivXGR}nN9l|pJT}x zcFbS$>l}lp{tT9PEKOr{%tOl#PMo=kUR$^gJdRQFzy(ZDMMcG=%c%bH4H0Zbx$}9i zO@Xn@*$8ZHqeH&&CU)(~t^$_;BQNLm!oos#1Cc~v(T*$fI_ITSaP<+K?P<9sjhWi> zT0f6THPC*$ed9PJ-&kgG)kfRmqF1;N%uJ$z5q<7Mf`*jJwbl!T=Eh066JondwP}ls zL#WP%dB9%w0r3yq92t$;eylej5eTn5Wgy{h_Sm=w6yT7cMSclpN(HZrnky~^Exik) zgR%#Iz3(mtCQ>;_k7Z^7|DO@W0s>2af@{vyQH5>Z1Rh z%SAm;b0qfyY8dzcin?{i1)cNRj+~}H?eh-IGMOiSlG*5V_ByD}?I$`CQ~&uoS4I^Q z3Cb@z5%VnUQ?zyseXw~Ao@MhqKHdg2Vl6=x*@8m7GuiZOykWvDUQVJsmH~CJf?4R@ zIsujfoVTJ|f17aX-(UdOUgpfrg<|=IEfRMI{Uk2CMl?%EoZGQgqTwj;u}9}5B=&s# zcRX+z`V%mdB%g`@BjDd-^?#QBOWf Date: Thu, 20 Aug 2026 12:36:11 +0200 Subject: [PATCH 09/18] fix(buy,sell): close the review findings on the amount field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six findings from the review lanes: An emptied amount no longer triggers a quote. Clearing the counter value made the listener sync the field to empty and fetch a quote for it, which chargedFiatAmount turns into 0 — and since no loading state is emitted while a success is present, the previous quote stayed confirmable during that request. The empty case now drops the landed quote instead. FormatException is caught around chargedFiatAmount only. It used to wrap the whole service call, where jsonDecode and DateTime.parse throw it too, so a malformed backend response was reported to the user as an unreadable amount. Pasting 1.000.000 now resolves like typing it. Consecutive groups with the same separator are unambiguous, and the two input paths disagreed. The caret keeps its position relative to the edit instead of jumping to the end, the sell error state gets the widget test and golden its buy counterpart already had, and the ARB files end in a newline again. --- assets/languages/strings_de.arb | 2 +- assets/languages/strings_en.arb | 2 +- lib/packages/utils/fiat_amount.dart | 13 +++- lib/screens/buy/buy_page.dart | 10 ++- .../buy_payment_info_cubit.dart | 20 ++++- .../sell_payment_info_cubit.dart | 24 +++--- lib/widgets/fiat_input_formatter.dart | 12 ++- .../screens/sell/sell_golden_test.dart | 31 ++++++++ test/packages/utils/fiat_amount_test.dart | 12 +++ .../buy/buy_amount_change_repro_test.dart | 76 +++++++++++++++++++ .../cubits/buy_payment_info_cubit_test.dart | 27 +++++++ .../cubits/sell_payment_info_cubit_test.dart | 16 ++++ .../sell/sell_button_call_site_test.dart | 52 +++++++++++++ test/widgets/fiat_input_formatter_test.dart | 67 ++++++++++++++++ 14 files changed, 342 insertions(+), 22 deletions(-) create mode 100644 test/widgets/fiat_input_formatter_test.dart diff --git a/assets/languages/strings_de.arb b/assets/languages/strings_de.arb index 074bf8a85..32f1760d8 100644 --- a/assets/languages/strings_de.arb +++ b/assets/languages/strings_de.arb @@ -467,4 +467,4 @@ "youPay": "Sie bezahlen", "youReceive": "Sie erhalten", "youSell": "Sie verkaufen" -} \ No newline at end of file +} diff --git a/assets/languages/strings_en.arb b/assets/languages/strings_en.arb index b869c0b13..66c8e5331 100644 --- a/assets/languages/strings_en.arb +++ b/assets/languages/strings_en.arb @@ -467,4 +467,4 @@ "youPay": "You pay", "youReceive": "You receive", "youSell": "You sell" -} \ No newline at end of file +} diff --git a/lib/packages/utils/fiat_amount.dart b/lib/packages/utils/fiat_amount.dart index b245f5b48..06898e2a0 100644 --- a/lib/packages/utils/fiat_amount.dart +++ b/lib/packages/utils/fiat_amount.dart @@ -7,13 +7,18 @@ double? tryParseFiatAmount(String input) { return double.tryParse(input.replaceAll(',', '.')); } -/// Strips a lone thousands group (`1.000` / `1,000` → `1000`). +/// Strips thousands grouping (`1.000` / `1,000` / `1.000.000` → integer). /// /// EUR and CHF have two decimal places, so a separator followed by exactly -/// three digits cannot be a decimal. Every other input is returned unchanged, -/// including partials (`1.`, `1.0`, `1.00`) and real decimals (`300,75`). +/// three digits cannot be a decimal. Consecutive groups with the same +/// separator are unambiguous thousands grouping, so a paste of `1.000.000` +/// matches what typing the same characters one by one already produced. +/// Mixed separators (`1.300,75`) and real decimals (`300,75`) are returned +/// unchanged, as are partials (`1.`, `1.0`, `1.00`). String normalizeFiatInput(String input) { - if (!RegExp(r'^\d+[.,]\d{3}$').hasMatch(input)) return input; + if (!RegExp(r'^(\d+)([.,])(\d{3}(?:\2\d{3})*)$').hasMatch(input)) { + return input; + } return input.replaceAll('.', '').replaceAll(',', ''); } diff --git a/lib/screens/buy/buy_page.dart b/lib/screens/buy/buy_page.dart index 2d827bf91..b62def54e 100644 --- a/lib/screens/buy/buy_page.dart +++ b/lib/screens/buy/buy_page.dart @@ -58,7 +58,15 @@ class _BuyViewState extends State { listener: (context, state) { _syncController(_amountController, state.fiatText); _syncController(_resultController, state.sharesText); - context.read().getPaymentInfo( + final paymentInfo = context.read(); + // An empty field is not an amount to quote. Fetching it would + // charge 0 and, while that request is in flight, leave a previous + // Success (and its confirm button) on screen. + if (_amountController.text.isEmpty) { + paymentInfo.clearQuote(); + return; + } + paymentInfo.getPaymentInfo( amount: _amountController.text, currency: state.currency, ); diff --git a/lib/screens/buy/cubits/buy_payment_info/buy_payment_info_cubit.dart b/lib/screens/buy/cubits/buy_payment_info/buy_payment_info_cubit.dart index 8fd155a80..c29c7ed04 100644 --- a/lib/screens/buy/cubits/buy_payment_info/buy_payment_info_cubit.dart +++ b/lib/screens/buy/cubits/buy_payment_info/buy_payment_info_cubit.dart @@ -58,10 +58,26 @@ class BuyPaymentInfoCubit extends Cubit { emit(newState); } + /// Drops a landed quote so it cannot be confirmed after the amount it + /// belonged to has disappeared. Cancels an in-flight fetch to keep a + /// later completion from restoring the old Success. + void clearQuote() { + _completer?.cancel(); + if (isClosed) return; + emit(const BuyPaymentInfoInitial()); + } + Future _runGetPaymentInfo(String amount, Currency currency) async { + final int charged; + try { + charged = chargedFiatAmount(amount); + } on FormatException { + return const BuyPaymentInfoFailure(PaymentInfoError.invalidAmountFormat); + } + try { final paymentInfo = await _buyPaymentInfoService.getPaymentInfo( - chargedFiatAmount(amount), + charged, currency: currency, ); @@ -113,8 +129,6 @@ class BuyPaymentInfoCubit extends Cubit { } developer.log(e.toString()); return BuyPaymentInfoFailure(PaymentInfoError.unknown, message: e.message); - } on FormatException { - return const BuyPaymentInfoFailure(PaymentInfoError.invalidAmountFormat); } catch (e) { developer.log(e.toString()); return BuyPaymentInfoFailure(PaymentInfoError.unknown, message: e.toString()); diff --git a/lib/screens/sell/cubits/sell_payment_info/sell_payment_info_cubit.dart b/lib/screens/sell/cubits/sell_payment_info/sell_payment_info_cubit.dart index a7ee76c1c..871b8c828 100644 --- a/lib/screens/sell/cubits/sell_payment_info/sell_payment_info_cubit.dart +++ b/lib/screens/sell/cubits/sell_payment_info/sell_payment_info_cubit.dart @@ -41,8 +41,22 @@ class SellPaymentInfoCubit extends Cubit { try { emit(const SellPaymentInfoLoading()); + final int charged; + try { + charged = chargedFiatAmount(amount); + } on FormatException catch (e) { + if (isClosed) return; + emit( + SellPaymentInfoFailure( + PaymentInfoError.invalidAmountFormat, + message: e.toString(), + ), + ); + return; + } + final paymentInfo = await _sellPaymentInfoService.getPaymentInfo( - chargedFiatAmount(amount), + charged, iban, currency: currency, ); @@ -115,14 +129,6 @@ class SellPaymentInfoCubit extends Cubit { message: e.message, ), ); - } on FormatException catch (e) { - if (isClosed) return; - emit( - SellPaymentInfoFailure( - PaymentInfoError.invalidAmountFormat, - message: e.toString(), - ), - ); } catch (e) { developer.log(e.toString()); if (isClosed) return; diff --git a/lib/widgets/fiat_input_formatter.dart b/lib/widgets/fiat_input_formatter.dart index 9e8d15691..05b3ce5b8 100644 --- a/lib/widgets/fiat_input_formatter.dart +++ b/lib/widgets/fiat_input_formatter.dart @@ -1,8 +1,9 @@ import 'package:flutter/services.dart'; import 'package:realunit_wallet/packages/utils/fiat_amount.dart'; -/// Digits and separators only. A completed thousands group (`1.000` / `1,000`) -/// is rewritten to the integer; partials (`1.`, `1.0`, `1.00`) stay as typed. +/// Digits and separators only. A completed thousands group (`1.000` / +/// `1,000` / `1.000.000`) is rewritten to the integer; partials (`1.`, +/// `1.0`, `1.00`) stay as typed. class FiatInputFormatter extends TextInputFormatter { const FiatInputFormatter(); @@ -13,9 +14,14 @@ class FiatInputFormatter extends TextInputFormatter { final allowed = _allowedChars.formatEditUpdate(oldValue, newValue); final normalized = normalizeFiatInput(allowed.text); if (normalized == allowed.text) return allowed; + final cursor = allowed.selection.baseOffset.clamp(0, allowed.text.length); + final separatorsBeforeCursor = + RegExp('[.,]').allMatches(allowed.text.substring(0, cursor)).length; return TextEditingValue( text: normalized, - selection: TextSelection.collapsed(offset: normalized.length), + selection: TextSelection.collapsed( + offset: (cursor - separatorsBeforeCursor).clamp(0, normalized.length), + ), ); } } diff --git a/test/goldens/screens/sell/sell_golden_test.dart b/test/goldens/screens/sell/sell_golden_test.dart index 948a4b81b..c5eab7a6a 100644 --- a/test/goldens/screens/sell/sell_golden_test.dart +++ b/test/goldens/screens/sell/sell_golden_test.dart @@ -241,5 +241,36 @@ void main() { return wrapForGolden(buildSubject()); }, ); + + goldenTest( + 'invalid amount format failure', + fileName: 'sell_invalid_amount_format', + constraints: const BoxConstraints.tightFor(width: 390, height: 844), + builder: () { + when(() => balanceCubit.state).thenReturn( + Balance( + chainId: 1, + contractAddress: '0x0', + walletAddress: '0x0', + balance: BigInt.from(1000000000000000000), + asset: realUnitAsset, + ), + ); + when(() => paymentInfoCubit.state).thenReturn( + const SellPaymentInfoFailure(PaymentInfoError.invalidAmountFormat), + ); + when(() => converterCubit.state).thenReturn( + const SellConverterState( + // Mixed grouping+decimal. The field formatter only rewrites + // thousands groups with the same separator; this value reaches + // chargedFiatAmount, which throws FormatException. + fiatText: '1.300,75', + sharesText: '', + currency: Currency.chf, + ), + ); + return wrapForGolden(buildSubject()); + }, + ); }); } diff --git a/test/packages/utils/fiat_amount_test.dart b/test/packages/utils/fiat_amount_test.dart index 5661c1c07..625a5db3f 100644 --- a/test/packages/utils/fiat_amount_test.dart +++ b/test/packages/utils/fiat_amount_test.dart @@ -47,6 +47,10 @@ void main() { test('returns null on multi-separator input', () { expect(tryParseFiatAmount('1.300,75'), isNull); + // Consecutive thousands groups are stripped by [normalizeFiatInput] + // before the parser sees them. The parser itself still rejects the + // raw grouped form. + expect(tryParseFiatAmount('1.000.000'), isNull); }); test('returns null on grouping-ambiguous input (separator + 3 digits)', () { @@ -68,11 +72,19 @@ void main() { expect(normalizeFiatInput('10,000'), '10000'); }); + test('strips consecutive thousands groups with the same separator', () { + expect(normalizeFiatInput('1.000.000'), '1000000'); + expect(normalizeFiatInput('1,000,000'), '1000000'); + expect(normalizeFiatInput('10.000.000'), '10000000'); + expect(normalizeFiatInput('1000.000.000'), '1000000000'); + }); + test('leaves every other input unchanged', () { expect(normalizeFiatInput('300'), '300'); expect(normalizeFiatInput('300,75'), '300,75'); expect(normalizeFiatInput('0,5'), '0,5'); expect(normalizeFiatInput('1.300,75'), '1.300,75'); + expect(normalizeFiatInput('1.000,000'), '1.000,000'); expect(normalizeFiatInput('3,5,7'), '3,5,7'); }); diff --git a/test/screens/buy/buy_amount_change_repro_test.dart b/test/screens/buy/buy_amount_change_repro_test.dart index 660db4ec7..35c725693 100644 --- a/test/screens/buy/buy_amount_change_repro_test.dart +++ b/test/screens/buy/buy_amount_change_repro_test.dart @@ -224,6 +224,82 @@ void main() { }, ); + testWidgets( + 'pasting 1.000.000 in one step is shown as 1000000 and does not show an error', + (tester) async { + await _pumpLoadedBuyPage(tester); + + await tester.enterText(_amountField, '1.000.000'); + await tester.pump(); + + expect( + tester.widget(_amountField).controller!.text, + '1000000', + reason: + 'Pasting 1.000.000 must be rewritten to 1000000 in one step, ' + 'matching character-by-character typing. ${_snapshot(tester)}', + ); + + await tester.pump(_afterDebounce); + await tester.pump(); + + expect( + find.text(S.current.invalidAmountFormatTitle), + findsNothing, + reason: + 'Pasting 1.000.000 must not surface the unreadable-amount hint. ' + '${_snapshot(tester)}', + ); + _expectHealthyQuote( + tester, + expectedAmount: '1000000', + reasonPrefix: + 'Pasting 1.000.000 must appear as 1000000 and keep a valid quote.', + ); + }, + ); + + testWidgets( + 'a failed shares-to-fiat conversion drops the leftover quote so it cannot be confirmed', + (tester) async { + await _pumpLoadedBuyPage(tester); + + expect( + find.byType(BuyConfirmButton), + findsOneWidget, + reason: + 'Default 300 must already show the binding-buy button. ' + '${_snapshot(tester)}', + ); + + when(() => brokerbotService.getBuyPrice(any(), any())).thenAnswer( + (_) async => throw Exception('BuyPrice request failed: conversion failed'), + ); + + await tester.enterText(find.byType(TextField).last, '10'); + await tester.pump(_afterDebounce); + await tester.pump(); + + expect( + find.byType(BuyConfirmButton), + findsNothing, + reason: + 'After getBuyPrice fails the leftover 300 quote must not stay ' + 'confirmable. ${_snapshot(tester)}', + ); + expect( + tester.element(find.byType(BuyView)).read().state, + isA(), + reason: + 'The payment-info cubit must drop the landed quote when the ' + 'fiat counterpart is cleared. ${_snapshot(tester)}', + ); + verifyNever( + () => paymentInfoService.getPaymentInfo(0, currency: any(named: 'currency')), + ); + }, + ); + testWidgets( 'buying after replacing 300 with 100 confirms the 100 quote, not the leftover 300', (tester) async { diff --git a/test/screens/buy/cubits/buy_payment_info_cubit_test.dart b/test/screens/buy/cubits/buy_payment_info_cubit_test.dart index 0f656cffe..fb1dab47a 100644 --- a/test/screens/buy/cubits/buy_payment_info_cubit_test.dart +++ b/test/screens/buy/cubits/buy_payment_info_cubit_test.dart @@ -191,6 +191,21 @@ void main() { verifyNever(() => service.getPaymentInfo(any(), currency: any(named: 'currency'))); }); + test('FormatException from the service is unknown, not invalidAmountFormat', () async { + when(() => service.getPaymentInfo(any(), currency: any(named: 'currency'))) + .thenAnswer((_) async => throw const FormatException('Unexpected character')); + + final cubit = build(); + await cubit.getPaymentInfo(amount: '300'); + + expect(cubit.state, isA()); + expect( + (cubit.state as BuyPaymentInfoFailure).error, + PaymentInfoError.unknown, + ); + verify(() => service.getPaymentInfo(300, currency: Currency.chf)).called(1); + }); + test('KycLevelRequiredException → Failure(kycRequired, requiredLevel)', () async { when(() => service.getPaymentInfo(any(), currency: any(named: 'currency'))) .thenAnswer( @@ -341,6 +356,18 @@ void main() { expect((cubit.state as BuyPaymentInfoFailure).message, 'bad'); }); + test('clearQuote drops a landed Success back to Initial', () async { + when(() => service.getPaymentInfo(any(), currency: any(named: 'currency'))) + .thenAnswer((_) async => _info()); + + final cubit = build(); + await cubit.getPaymentInfo(amount: '300'); + expect(cubit.state, isA()); + + cubit.clearQuote(); + expect(cubit.state, isA()); + }); + test('does not emit after close', () async { final completer = Completer(); when(() => service.getPaymentInfo(any(), currency: any(named: 'currency'))) diff --git a/test/screens/sell/cubits/sell_payment_info_cubit_test.dart b/test/screens/sell/cubits/sell_payment_info_cubit_test.dart index 9bec37020..df8989633 100644 --- a/test/screens/sell/cubits/sell_payment_info_cubit_test.dart +++ b/test/screens/sell/cubits/sell_payment_info_cubit_test.dart @@ -361,6 +361,22 @@ void main() { verifyNever(() => service.getPaymentInfo(any(), any(), currency: any(named: 'currency'))); }); + test('FormatException from the service is unknown, not invalidAmountFormat', () async { + when( + () => service.getPaymentInfo(any(), any(), currency: any(named: 'currency')), + ).thenAnswer((_) async => throw const FormatException('Unexpected character')); + + final cubit = build(); + await cubit.getPaymentInfo(amount: '100', iban: 'CH56'); + + expect(cubit.state, isA()); + expect( + (cubit.state as SellPaymentInfoFailure).error, + PaymentInfoError.unknown, + ); + verify(() => service.getPaymentInfo(100, 'CH56', currency: Currency.chf)).called(1); + }); + test('does not emit after close', () async { final completer = Completer(); when( diff --git a/test/screens/sell/sell_button_call_site_test.dart b/test/screens/sell/sell_button_call_site_test.dart index 6ff6dfb0d..81558f3cf 100644 --- a/test/screens/sell/sell_button_call_site_test.dart +++ b/test/screens/sell/sell_button_call_site_test.dart @@ -13,6 +13,7 @@ import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; import 'package:mocktail/mocktail.dart'; import 'package:realunit_wallet/generated/i18n.dart'; +import 'package:realunit_wallet/packages/service/dfx/models/payment/payment_info_error.dart'; import 'package:realunit_wallet/packages/service/dfx/models/payment/sell/dto/eip7702/eip7702_data_dto.dart'; import 'package:realunit_wallet/packages/service/dfx/models/payment/sell/dto/real_unit_sell_payment_info_dto.dart'; import 'package:realunit_wallet/packages/service/dfx/models/payment/sell/sell_payment_info.dart'; @@ -213,5 +214,56 @@ void main() { ); }, ); + + testWidgets( + 'shows a SnackBar with invalidAmountFormatDescription on invalidAmountFormat', + (tester) async { + whenListen( + sellPaymentInfoCubit, + Stream.fromIterable([ + const SellPaymentInfoFailure(PaymentInfoError.invalidAmountFormat), + ]), + initialState: const SellPaymentInfoInitial(), + ); + + final router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, _) => Scaffold( + body: Center( + child: MultiBlocProvider( + providers: [ + BlocProvider.value(value: sellPaymentInfoCubit), + BlocProvider.value(value: sellConverterCubit), + ], + child: const SellButton(amount: '10', bankAccount: null), + ), + ), + ), + ), + ], + ); + addTearDown(router.dispose); + + await tester.pumpWidget( + MaterialApp.router( + theme: realUnitTheme, + routerConfig: router, + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: S.delegate.supportedLocales, + ), + ); + await tester.pump(); + + expect(find.byType(SnackBar), findsOneWidget); + expect(find.text(S.current.invalidAmountFormatDescription), findsOneWidget); + }, + ); }); } diff --git a/test/widgets/fiat_input_formatter_test.dart b/test/widgets/fiat_input_formatter_test.dart new file mode 100644 index 000000000..ac58743b8 --- /dev/null +++ b/test/widgets/fiat_input_formatter_test.dart @@ -0,0 +1,67 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:realunit_wallet/widgets/fiat_input_formatter.dart'; + +void main() { + const formatter = FiatInputFormatter(); + + TextEditingValue update(TextEditingValue oldValue, TextEditingValue newValue) => + formatter.formatEditUpdate(oldValue, newValue); + + group('$FiatInputFormatter.formatEditUpdate', () { + test('rewrites a one-step paste of 1.000.000 to 1000000', () { + final out = update( + TextEditingValue.empty, + const TextEditingValue( + text: '1.000.000', + selection: TextSelection.collapsed(offset: 9), + ), + ); + expect(out.text, '1000000'); + expect(out.selection, const TextSelection.collapsed(offset: 7)); + }); + + test('rewrites a one-step paste of 1,000,000 to 1000000', () { + final out = update( + TextEditingValue.empty, + const TextEditingValue( + text: '1,000,000', + selection: TextSelection.collapsed(offset: 9), + ), + ); + expect(out.text, '1000000'); + expect(out.selection, const TextSelection.collapsed(offset: 7)); + }); + + test('keeps the caret at the end when a thousands group is completed at the end', () { + final out = update( + const TextEditingValue( + text: '1.00', + selection: TextSelection.collapsed(offset: 4), + ), + const TextEditingValue( + text: '1.000', + selection: TextSelection.collapsed(offset: 5), + ), + ); + expect(out.text, '1000'); + expect(out.selection, const TextSelection.collapsed(offset: 4)); + }); + + test('keeps the caret relative to the edit when a thousands group is stripped mid-text', () { + // Insert '.' after '1' in '1000' → '1.000' → '1000', caret stays after '1'. + final out = update( + const TextEditingValue( + text: '1000', + selection: TextSelection.collapsed(offset: 1), + ), + const TextEditingValue( + text: '1.000', + selection: TextSelection.collapsed(offset: 2), + ), + ); + expect(out.text, '1000'); + expect(out.selection, const TextSelection.collapsed(offset: 1)); + }); + }); +} From cd40e64f862368c6c0b8585a0150b3af6573b94f Mon Sep 17 00:00:00 2001 From: joshuakrueger-dfx Date: Thu, 20 Aug 2026 12:41:30 +0200 Subject: [PATCH 10/18] test(goldens): add the sell unreadable-amount baseline Rendered by golden-regenerate on the self-hosted runner, committed here signed rather than pushed by the workflow bot. --- .../macos/sell_invalid_amount_format.png | Bin 0 -> 23148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/goldens/screens/sell/goldens/macos/sell_invalid_amount_format.png diff --git a/test/goldens/screens/sell/goldens/macos/sell_invalid_amount_format.png b/test/goldens/screens/sell/goldens/macos/sell_invalid_amount_format.png new file mode 100644 index 0000000000000000000000000000000000000000..9285faf87793dfd8a5642ebdbd703d554650be95 GIT binary patch literal 23148 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 Date: Thu, 20 Aug 2026 12:50:47 +0200 Subject: [PATCH 11/18] test(goldens): drop the sell baseline that could never fail The generated sell_invalid_amount_format.png came out bit-identical to sell_unknown_error.png: on the sell screen the error is a SnackBar from the listener, and a state-stubbed golden does not render it, so the image shows an unchanged screen. A golden that cannot fail hides the gap it was meant to close. The SnackBar is covered by the widget test instead, and a comment marks why no golden exists here. --- .../macos/sell_invalid_amount_format.png | Bin 23148 -> 0 bytes .../screens/sell/sell_golden_test.dart | 34 +++--------------- 2 files changed, 4 insertions(+), 30 deletions(-) delete mode 100644 test/goldens/screens/sell/goldens/macos/sell_invalid_amount_format.png diff --git a/test/goldens/screens/sell/goldens/macos/sell_invalid_amount_format.png b/test/goldens/screens/sell/goldens/macos/sell_invalid_amount_format.png deleted file mode 100644 index 9285faf87793dfd8a5642ebdbd703d554650be95..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23148 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 balanceCubit.state).thenReturn( - Balance( - chainId: 1, - contractAddress: '0x0', - walletAddress: '0x0', - balance: BigInt.from(1000000000000000000), - asset: realUnitAsset, - ), - ); - when(() => paymentInfoCubit.state).thenReturn( - const SellPaymentInfoFailure(PaymentInfoError.invalidAmountFormat), - ); - when(() => converterCubit.state).thenReturn( - const SellConverterState( - // Mixed grouping+decimal. The field formatter only rewrites - // thousands groups with the same separator; this value reaches - // chargedFiatAmount, which throws FormatException. - fiatText: '1.300,75', - sharesText: '', - currency: Currency.chf, - ), - ); - return wrapForGolden(buildSubject()); - }, - ); + // No golden for PaymentInfoError.invalidAmountFormat: the error is a + // SnackBar from SellButton's BlocConsumer listener. A state-stubbed + // golden never fires the listener, so the image would be identical to + // another failure state's. Covered by sell_button_call_site_test.dart. }); } From e3e20b5cbc5f9fab6a1a010ec464ddf20f4d2b3f Mon Sep 17 00:00:00 2001 From: joshuakrueger-dfx Date: Thu, 20 Aug 2026 14:11:25 +0200 Subject: [PATCH 12/18] fix(buy,sell): resolve mixed separators and restore the sell golden The sell golden was removed on a premise the repo disproves: whenListen drives a BlocConsumer through the transition so a listener-triggered SnackBar does render, which is how the twelve existing _snackbar baselines work. It is back, built that way. Pasting 1.000,50 disagreed with typing it: the paste never matched the thousands rule, so the parser saw 1.000.50 and rejected it, while typing had already normalised the intermediate 1.000 to 1000. Thousands grouping followed by one or two decimals of the other separator now resolves atomically. _expectHealthyQuote also asserts the healthy state is present, not just that no error is, and the buy golden comment matches the widened rule. --- lib/packages/utils/fiat_amount.dart | 17 +- lib/widgets/fiat_input_formatter.dart | 3 +- test/goldens/screens/buy/buy_golden_test.dart | 9 +- .../screens/sell/sell_golden_test.dart | 5 - .../screens/sell/sell_states_golden_test.dart | 27 ++ test/packages/utils/fiat_amount_test.dart | 14 +- .../buy/buy_amount_change_repro_test.dart | 276 ++++++++++++++++++ test/widgets/fiat_input_formatter_test.dart | 34 +++ 8 files changed, 370 insertions(+), 15 deletions(-) diff --git a/lib/packages/utils/fiat_amount.dart b/lib/packages/utils/fiat_amount.dart index 06898e2a0..30e4599ab 100644 --- a/lib/packages/utils/fiat_amount.dart +++ b/lib/packages/utils/fiat_amount.dart @@ -13,13 +13,20 @@ double? tryParseFiatAmount(String input) { /// three digits cannot be a decimal. Consecutive groups with the same /// separator are unambiguous thousands grouping, so a paste of `1.000.000` /// matches what typing the same characters one by one already produced. -/// Mixed separators (`1.300,75`) and real decimals (`300,75`) are returned -/// unchanged, as are partials (`1.`, `1.0`, `1.00`). +/// Mixed thousands + 1–2 decimal digits of the other separator (`1.000,50` / +/// `1,000.50`) drop the thousands separator and keep the decimal, so paste +/// matches typing. Real decimals (`300,75`) and partials (`1.`, `1.0`, +/// `1.00`) are returned unchanged, as is mixed input that is not uniquely +/// thousands-then-decimal (`1.000,000`, `3,5,7`). String normalizeFiatInput(String input) { - if (!RegExp(r'^(\d+)([.,])(\d{3}(?:\2\d{3})*)$').hasMatch(input)) { - return input; + if (RegExp(r'^(\d+)([.,])(\d{3}(?:\2\d{3})*)$').hasMatch(input)) { + return input.replaceAll('.', '').replaceAll(',', ''); } - return input.replaceAll('.', '').replaceAll(',', ''); + final mixed = RegExp(r'^(\d+)([.,])(\d{3}(?:\2\d{3})*)([.,])(\d{1,2})$').firstMatch(input); + if (mixed != null && mixed[2] != mixed[4]) { + return input.replaceAll(mixed[2]!, ''); + } + return input; } /// The whole-currency integer the backend charges for the raw [input] the user diff --git a/lib/widgets/fiat_input_formatter.dart b/lib/widgets/fiat_input_formatter.dart index 05b3ce5b8..771178e93 100644 --- a/lib/widgets/fiat_input_formatter.dart +++ b/lib/widgets/fiat_input_formatter.dart @@ -2,7 +2,8 @@ import 'package:flutter/services.dart'; import 'package:realunit_wallet/packages/utils/fiat_amount.dart'; /// Digits and separators only. A completed thousands group (`1.000` / -/// `1,000` / `1.000.000`) is rewritten to the integer; partials (`1.`, +/// `1,000` / `1.000.000`) is rewritten to the integer; mixed thousands plus +/// decimal (`1.000,50` / `1,000.50`) keep the decimal; partials (`1.`, /// `1.0`, `1.00`) stay as typed. class FiatInputFormatter extends TextInputFormatter { const FiatInputFormatter(); diff --git a/test/goldens/screens/buy/buy_golden_test.dart b/test/goldens/screens/buy/buy_golden_test.dart index 664056428..57554ce82 100644 --- a/test/goldens/screens/buy/buy_golden_test.dart +++ b/test/goldens/screens/buy/buy_golden_test.dart @@ -280,9 +280,12 @@ void main() { ); when(() => converterCubit.state).thenReturn( const BuyConverterState( - // Mixed grouping+decimal. The field formatter only rewrites a - // lone thousands group (`1.000` → `1000`); this value reaches - // chargedFiatAmount, which throws FormatException. + // This golden stubs the failure panel and does not run the + // formatter. Same-separator thousands groups (`1.000`, + // `1.000.000`) and mixed thousands + 1–2 decimal digits of the + // other separator (`1.000,50`, `1.300,75`) are stripped before + // the parser; a 3-digit mixed tail (`1.000,000`) still reaches + // chargedFiatAmount and throws FormatException. fiatText: '1.300,75', sharesText: '', currency: Currency.chf, diff --git a/test/goldens/screens/sell/sell_golden_test.dart b/test/goldens/screens/sell/sell_golden_test.dart index 7593d2920..948a4b81b 100644 --- a/test/goldens/screens/sell/sell_golden_test.dart +++ b/test/goldens/screens/sell/sell_golden_test.dart @@ -241,10 +241,5 @@ void main() { return wrapForGolden(buildSubject()); }, ); - - // No golden for PaymentInfoError.invalidAmountFormat: the error is a - // SnackBar from SellButton's BlocConsumer listener. A state-stubbed - // golden never fires the listener, so the image would be identical to - // another failure state's. Covered by sell_button_call_site_test.dart. }); } diff --git a/test/goldens/screens/sell/sell_states_golden_test.dart b/test/goldens/screens/sell/sell_states_golden_test.dart index f7b565644..1a4f7a84e 100644 --- a/test/goldens/screens/sell/sell_states_golden_test.dart +++ b/test/goldens/screens/sell/sell_states_golden_test.dart @@ -14,6 +14,7 @@ import 'package:realunit_wallet/packages/service/dfx/dfx_brokerbot_service.dart' import 'package:realunit_wallet/packages/service/dfx/dfx_price_service.dart'; import 'package:realunit_wallet/packages/service/dfx/models/bank_account/bank_account.dart'; import 'package:realunit_wallet/packages/service/dfx/models/bank_account/dto/bank_account_dto.dart'; +import 'package:realunit_wallet/packages/service/dfx/models/payment/payment_info_error.dart'; import 'package:realunit_wallet/packages/service/dfx/models/payment/sell/dto/eip7702/eip7702_data_dto.dart'; import 'package:realunit_wallet/packages/service/dfx/models/payment/sell/dto/real_unit_sell_payment_info_dto.dart'; import 'package:realunit_wallet/packages/service/dfx/models/payment/sell/sell_payment_info.dart'; @@ -239,6 +240,32 @@ void main() { constraints: phoneConstraints, builder: () => wrapForGolden(buildSubject()), ); + + // SellButton's BlocConsumer shows the invalidAmountFormat SnackBar on the + // transition into that failure (sell_button.dart). Seed Initial and emit + // the failure so the listener fires; do not stub `state` to the final + // value — that would erase the transition. The single `pump` delivers the + // emission and `pumpAndSettle` runs the SnackBar entrance (the 4s + // auto-dismiss is a Timer, not a frame, so it stays visible). + goldenTest( + 'invalid amount format SnackBar', + fileName: 'sell_invalid_amount_format_snackbar', + constraints: phoneConstraints, + pumpBeforeTest: (tester) async { + await tester.pump(); + await tester.pumpAndSettle(); + }, + builder: () { + whenListen( + paymentInfoCubit, + Stream.value( + const SellPaymentInfoFailure(PaymentInfoError.invalidAmountFormat), + ), + initialState: const SellPaymentInfoInitial(), + ); + return wrapForGolden(buildSubject()); + }, + ); }); // ---- B) SellConfirmSheet (bottom sheet after payment-info success) ---- diff --git a/test/packages/utils/fiat_amount_test.dart b/test/packages/utils/fiat_amount_test.dart index 625a5db3f..150397a31 100644 --- a/test/packages/utils/fiat_amount_test.dart +++ b/test/packages/utils/fiat_amount_test.dart @@ -79,12 +79,24 @@ void main() { expect(normalizeFiatInput('1000.000.000'), '1000000000'); }); + test('strips mixed thousands grouping plus a decimal of the other separator', () { + expect(normalizeFiatInput('1.000,50'), '1000,50'); + expect(normalizeFiatInput('1,000.50'), '1000.50'); + expect(normalizeFiatInput('1.000,5'), '1000,5'); + expect(normalizeFiatInput('1,000.5'), '1000.5'); + expect(normalizeFiatInput('1.300,75'), '1300,75'); + expect(normalizeFiatInput('1.000.000,50'), '1000000,50'); + expect(normalizeFiatInput('1,000,000.50'), '1000000.50'); + }); + test('leaves every other input unchanged', () { expect(normalizeFiatInput('300'), '300'); expect(normalizeFiatInput('300,75'), '300,75'); expect(normalizeFiatInput('0,5'), '0,5'); - expect(normalizeFiatInput('1.300,75'), '1.300,75'); expect(normalizeFiatInput('1.000,000'), '1.000,000'); + expect(normalizeFiatInput('1,000.000'), '1,000.000'); + expect(normalizeFiatInput('1.000.50'), '1.000.50'); + expect(normalizeFiatInput('1,000,50'), '1,000,50'); expect(normalizeFiatInput('3,5,7'), '3,5,7'); }); diff --git a/test/screens/buy/buy_amount_change_repro_test.dart b/test/screens/buy/buy_amount_change_repro_test.dart index 35c725693..7aec98189 100644 --- a/test/screens/buy/buy_amount_change_repro_test.dart +++ b/test/screens/buy/buy_amount_change_repro_test.dart @@ -259,6 +259,240 @@ void main() { }, ); + testWidgets( + 'pasting 1.000,50 is shown as 1000,50 and matches typing the same characters', + (tester) async { + await _pumpLoadedBuyPage(tester); + + await tester.enterText(_amountField, '1.000,50'); + await tester.pump(); + + expect( + tester.widget(_amountField).controller!.text, + '1000,50', + reason: + 'Pasting 1.000,50 must be rewritten to 1000,50 in one step, ' + 'matching character-by-character typing. ${_snapshot(tester)}', + ); + + await tester.pump(_afterDebounce); + await tester.pump(); + + _expectHealthyQuote( + tester, + expectedAmount: '1000,50', + reasonPrefix: + 'Pasting 1.000,50 must appear as 1000,50 and keep a valid quote.', + ); + }, + ); + + testWidgets( + 'typing 1.000,50 character by character ends as 1000,50 with a valid quote', + (tester) async { + await _pumpLoadedBuyPage(tester); + await tester.enterText(_amountField, ''); + await tester.pump(); + + for (final value in ['1', '1.', '1.0', '1.00']) { + await tester.enterText(_amountField, value); + await tester.pump(); + expect( + tester.widget(_amountField).controller!.text, + value, + reason: + 'Intermediate "$value" must stay as typed. ${_snapshot(tester)}', + ); + } + + await tester.enterText(_amountField, '1.000'); + await tester.pump(); + expect( + tester.widget(_amountField).controller!.text, + '1000', + reason: + 'The last character of 1.000 must rewrite the field to 1000. ' + '${_snapshot(tester)}', + ); + + for (final value in ['1000,', '1000,5', '1000,50']) { + await tester.enterText(_amountField, value); + await tester.pump(); + expect( + tester.widget(_amountField).controller!.text, + value, + reason: + 'Continuation "$value" must stay as typed. ${_snapshot(tester)}', + ); + } + + await tester.pump(_afterDebounce); + await tester.pump(); + + _expectHealthyQuote( + tester, + expectedAmount: '1000,50', + reasonPrefix: + 'Typing 1.000,50 must appear as 1000,50 and keep a valid quote, ' + 'matching a one-step paste of the same characters.', + ); + }, + ); + + testWidgets( + 'pasting 1,000.50 is shown as 1000.50 and matches typing the same characters', + (tester) async { + await _pumpLoadedBuyPage(tester); + + await tester.enterText(_amountField, '1,000.50'); + await tester.pump(); + + expect( + tester.widget(_amountField).controller!.text, + '1000.50', + reason: + 'Pasting 1,000.50 must be rewritten to 1000.50 in one step, ' + 'matching character-by-character typing. ${_snapshot(tester)}', + ); + + await tester.pump(_afterDebounce); + await tester.pump(); + + _expectHealthyQuote( + tester, + expectedAmount: '1000.50', + reasonPrefix: + 'Pasting 1,000.50 must appear as 1000.50 and keep a valid quote.', + ); + }, + ); + + testWidgets( + 'typing 1,000.50 character by character ends as 1000.50 with a valid quote', + (tester) async { + await _pumpLoadedBuyPage(tester); + await tester.enterText(_amountField, ''); + await tester.pump(); + + for (final value in ['1', '1,', '1,0', '1,00']) { + await tester.enterText(_amountField, value); + await tester.pump(); + expect( + tester.widget(_amountField).controller!.text, + value, + reason: + 'Intermediate "$value" must stay as typed. ${_snapshot(tester)}', + ); + } + + await tester.enterText(_amountField, '1,000'); + await tester.pump(); + expect( + tester.widget(_amountField).controller!.text, + '1000', + reason: + 'The last character of 1,000 must rewrite the field to 1000. ' + '${_snapshot(tester)}', + ); + + for (final value in ['1000.', '1000.5', '1000.50']) { + await tester.enterText(_amountField, value); + await tester.pump(); + expect( + tester.widget(_amountField).controller!.text, + value, + reason: + 'Continuation "$value" must stay as typed. ${_snapshot(tester)}', + ); + } + + await tester.pump(_afterDebounce); + await tester.pump(); + + _expectHealthyQuote( + tester, + expectedAmount: '1000.50', + reasonPrefix: + 'Typing 1,000.50 must appear as 1000.50 and keep a valid quote, ' + 'matching a one-step paste of the same characters.', + ); + }, + ); + + testWidgets( + 'pasting grouping-ambiguous 1.000,000 is rejected', + (tester) async { + await _pumpLoadedBuyPage(tester); + + await tester.enterText(_amountField, '1.000,000'); + await tester.pump(); + + expect( + tester.widget(_amountField).controller!.text, + '1.000,000', + reason: + 'Pasting 1.000,000 must stay as typed; mixed 3-digit tails are ' + 'grouping-ambiguous. ${_snapshot(tester)}', + ); + + await tester.pump(_afterDebounce); + await tester.pump(); + + final snapshot = _snapshot(tester); + expect( + find.text(S.current.invalidAmountFormatTitle), + findsOneWidget, + reason: + 'Pasting 1.000,000 must surface the unreadable-amount hint. ' + '$snapshot', + ); + expect( + find.byType(BuyConfirmButton), + findsNothing, + reason: + 'A grouping-ambiguous paste must not leave a confirmable quote. ' + '$snapshot', + ); + }, + ); + + testWidgets( + 'pasting 1.000.50 (same separator as thousands and decimal) is rejected', + (tester) async { + await _pumpLoadedBuyPage(tester); + + await tester.enterText(_amountField, '1.000.50'); + await tester.pump(); + + expect( + tester.widget(_amountField).controller!.text, + '1.000.50', + reason: + 'Pasting 1.000.50 must stay as typed; the same separator cannot ' + 'be both thousands grouping and a decimal. ${_snapshot(tester)}', + ); + + await tester.pump(_afterDebounce); + await tester.pump(); + + final snapshot = _snapshot(tester); + expect( + find.text(S.current.invalidAmountFormatTitle), + findsOneWidget, + reason: + 'Pasting 1.000.50 must surface the unreadable-amount hint. ' + '$snapshot', + ); + expect( + find.byType(BuyConfirmButton), + findsNothing, + reason: + 'An ambiguous same-separator paste must not leave a confirmable ' + 'quote. $snapshot', + ); + }, + ); + testWidgets( 'a failed shares-to-fiat conversion drops the leftover quote so it cannot be confirmed', (tester) async { @@ -352,6 +586,7 @@ void main() { _expectHealthyQuote( tester, expectedAmount: '300', + expectedCurrency: Currency.eur, reasonPrefix: 'After switching to EUR at 300 the quote must stay valid.', ); expect( @@ -401,6 +636,7 @@ void main() { _expectHealthyQuote( tester, expectedAmount: '100', + expectedCurrency: Currency.eur, reasonPrefix: 'After switching to EUR and replacing 300 with 100 the quote ' 'must stay valid.', @@ -655,6 +891,7 @@ void _expectHealthyQuote( WidgetTester tester, { required String expectedAmount, required String reasonPrefix, + Currency expectedCurrency = Currency.chf, }) { final snapshot = _snapshot(tester); final amount = tester.widget(_amountField); @@ -679,6 +916,45 @@ void _expectHealthyQuote( expectedAmount, reason: '$reasonPrefix Amount field is not "$expectedAmount". $snapshot', ); + + final paymentState = + tester.element(find.byType(BuyView)).read().state; + expect( + paymentState, + isA(), + reason: '$reasonPrefix Payment info is not a landed quote. $snapshot', + ); + final success = paymentState as BuyPaymentInfoSuccess; + expect( + success.buyPaymentInfo.amount, + chargedFiatAmount(expectedAmount).toDouble(), + reason: + '$reasonPrefix Quote amount is ${success.buyPaymentInfo.amount}, not ' + '${chargedFiatAmount(expectedAmount)}. $snapshot', + ); + expect( + success.buyPaymentInfo.currency, + expectedCurrency, + reason: + '$reasonPrefix Quote currency is ${success.buyPaymentInfo.currency.code}, ' + 'not ${expectedCurrency.code}. $snapshot', + ); + expect( + find.byType(BuyConfirmButton), + findsOneWidget, + reason: '$reasonPrefix Binding-buy button is missing. $snapshot', + ); + final filled = tester.widget( + find.descendant( + of: find.byType(BuyConfirmButton), + matching: find.byType(AppFilledButton), + ), + ); + expect( + filled.onPressed, + isNotNull, + reason: '$reasonPrefix Binding-buy button is not pressable. $snapshot', + ); } String _snapshot(WidgetTester tester) { diff --git a/test/widgets/fiat_input_formatter_test.dart b/test/widgets/fiat_input_formatter_test.dart index ac58743b8..98ea3dd90 100644 --- a/test/widgets/fiat_input_formatter_test.dart +++ b/test/widgets/fiat_input_formatter_test.dart @@ -33,6 +33,40 @@ void main() { expect(out.selection, const TextSelection.collapsed(offset: 7)); }); + test('rewrites a one-step paste of 1.000,50 to 1000,50', () { + final out = update( + TextEditingValue.empty, + const TextEditingValue( + text: '1.000,50', + selection: TextSelection.collapsed(offset: 8), + ), + ); + expect(out.text, '1000,50'); + }); + + test('rewrites a one-step paste of 1,000.50 to 1000.50', () { + final out = update( + TextEditingValue.empty, + const TextEditingValue( + text: '1,000.50', + selection: TextSelection.collapsed(offset: 8), + ), + ); + expect(out.text, '1000.50'); + }); + + test('leaves a one-step paste of grouping-ambiguous 1.000,000 unchanged', () { + final out = update( + TextEditingValue.empty, + const TextEditingValue( + text: '1.000,000', + selection: TextSelection.collapsed(offset: 9), + ), + ); + expect(out.text, '1.000,000'); + expect(out.selection, const TextSelection.collapsed(offset: 9)); + }); + test('keeps the caret at the end when a thousands group is completed at the end', () { final out = update( const TextEditingValue( From 26a133cdea1aa96a5e91f07c12bcb98ec269173b Mon Sep 17 00:00:00 2001 From: joshuakrueger-dfx Date: Thu, 20 Aug 2026 14:16:45 +0200 Subject: [PATCH 13/18] test(goldens): add the sell snackbar baseline Rendered by golden-regenerate on the self-hosted runner, committed here signed rather than pushed by the workflow bot. --- .../sell_invalid_amount_format_snackbar.png | Bin 0 -> 30162 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/goldens/screens/sell/goldens/macos/sell_invalid_amount_format_snackbar.png diff --git a/test/goldens/screens/sell/goldens/macos/sell_invalid_amount_format_snackbar.png b/test/goldens/screens/sell/goldens/macos/sell_invalid_amount_format_snackbar.png new file mode 100644 index 0000000000000000000000000000000000000000..f1fbfbca25d17145e2c22a235a93906728c1a29d GIT binary patch literal 30162 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 Date: Thu, 20 Aug 2026 14:18:43 +0200 Subject: [PATCH 14/18] fix(l10n): state the accepted form instead of the rejected condition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The notice named the condition that was rejected, and every widening of the normalisation made that wording wrong again — most recently 1.000,50, which carries two separators and is now accepted. Naming the accepted form instead keeps the text true independently of what the parser tightens or relaxes. --- assets/languages/strings_de.arb | 2 +- assets/languages/strings_en.arb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/languages/strings_de.arb b/assets/languages/strings_de.arb index 32f1760d8..0203d33b9 100644 --- a/assets/languages/strings_de.arb +++ b/assets/languages/strings_de.arb @@ -111,7 +111,7 @@ "identityCheckProcessDescription": "Als nächstes müssen Sie Ihre Identität verifizieren. Bitte halten Sie Ihren Ausweis bereit und erlauben Sie den Kamerazugriff auf dem Gerät.", "identityCheckRequired": "Identitätsprüfung erforderlich", "imprint": "Impressum", - "invalidAmountFormatDescription": "Bitte geben Sie den Betrag mit höchstens einem Trennzeichen ein, zum Beispiel 1000 oder 1000,50.", + "invalidAmountFormatDescription": "Bitte geben Sie den Betrag zum Beispiel als 1000 oder 1000,50 ein.", "invalidAmountFormatTitle": "Betrag nicht lesbar", "kyc": "Eröffnungsprozess", "kycAccountMergeDescription": "Ihre Identität wurde bereits in einem anderen Konto gefunden. Eine Zusammenführungsanfrage wurde erstellt. Bitte bestätigen Sie diese über die E-Mail, die Sie erhalten haben.", diff --git a/assets/languages/strings_en.arb b/assets/languages/strings_en.arb index 66c8e5331..8bbfb792e 100644 --- a/assets/languages/strings_en.arb +++ b/assets/languages/strings_en.arb @@ -111,7 +111,7 @@ "identityCheckProcessDescription": "Next, you need to verify your identity. Please have your ID ready and allow camera access on your device.", "identityCheckRequired": "Identity check required", "imprint": "Imprint", - "invalidAmountFormatDescription": "Please enter the amount with at most one separator, for example 1000 or 1000.50.", + "invalidAmountFormatDescription": "Please enter the amount as 1000 or 1000.50, for example.", "invalidAmountFormatTitle": "Amount is not readable", "kyc": "Onboarding process", "kycAccountMergeDescription": "Your identity was found in another account. A merge request has been created. Please confirm it via the email you received.", From fc63155eeb5b088092c7d8888d5445cef2f799ca Mon Sep 17 00:00:00 2001 From: joshuakrueger-dfx Date: Thu, 20 Aug 2026 14:24:00 +0200 Subject: [PATCH 15/18] test(goldens): refresh both baselines for the reworded notice Rendered by golden-regenerate on the self-hosted runner, committed here signed rather than pushed by the workflow bot. --- .../macos/buy_invalid_amount_format.png | Bin 26533 -> 23980 bytes .../sell_invalid_amount_format_snackbar.png | Bin 30162 -> 28270 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/test/goldens/screens/buy/goldens/macos/buy_invalid_amount_format.png b/test/goldens/screens/buy/goldens/macos/buy_invalid_amount_format.png index 4f23578ffcf06fa5bfeb1657b1f12b616681667f..e100bd6e51a266627c3c4be5e0002118278e8e14 100644 GIT binary patch literal 23980 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 K)P48kum1z_Kz@V( literal 26533 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 OkfoX3<+6)6{`?<0(e@|+ diff --git a/test/goldens/screens/sell/goldens/macos/sell_invalid_amount_format_snackbar.png b/test/goldens/screens/sell/goldens/macos/sell_invalid_amount_format_snackbar.png index f1fbfbca25d17145e2c22a235a93906728c1a29d..1ea51117a9ce303f3e0ed18ea7660e86d47f97e9 100644 GIT binary patch delta 11400 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 Date: Thu, 20 Aug 2026 15:08:16 +0200 Subject: [PATCH 16/18] fix(buy,sell): drop the unreachable sell error branch and fix the caret MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SellButton.amount comes from a digitsOnly field, so chargedFiatAmount can never throw on the sell path — the invalidAmountFormat branch there was dead code, reachable only by injecting the state in a test. Branch, snackbar, its widget test and its golden are gone; the formatter on the sell fiat field and the stale-counter clearing stay, they are independent. The mixed-separator normalisation moved the caret one place too early: the formatter counted every separator before it while only the thousands one is removed. It now uses the actual length difference, and the tests assert the selection, not just the text. The buy golden fixture used 1.300,75, which the widened rule now normalises away; it uses a value that still reaches the parser. --- .../sell_payment_info_cubit.dart | 16 +----- lib/screens/sell/widgets/sell_button.dart | 11 ---- lib/widgets/fiat_input_formatter.dart | 8 ++- test/goldens/screens/buy/buy_golden_test.dart | 12 ++-- .../sell_invalid_amount_format_snackbar.png | Bin 28270 -> 0 bytes .../screens/sell/sell_states_golden_test.dart | 27 --------- .../cubits/sell_payment_info_cubit_test.dart | 12 ---- .../sell/sell_button_call_site_test.dart | 52 ------------------ test/widgets/fiat_input_formatter_test.dart | 24 ++++++++ 9 files changed, 36 insertions(+), 126 deletions(-) delete mode 100644 test/goldens/screens/sell/goldens/macos/sell_invalid_amount_format_snackbar.png diff --git a/lib/screens/sell/cubits/sell_payment_info/sell_payment_info_cubit.dart b/lib/screens/sell/cubits/sell_payment_info/sell_payment_info_cubit.dart index 871b8c828..2c4ba5d82 100644 --- a/lib/screens/sell/cubits/sell_payment_info/sell_payment_info_cubit.dart +++ b/lib/screens/sell/cubits/sell_payment_info/sell_payment_info_cubit.dart @@ -41,22 +41,8 @@ class SellPaymentInfoCubit extends Cubit { try { emit(const SellPaymentInfoLoading()); - final int charged; - try { - charged = chargedFiatAmount(amount); - } on FormatException catch (e) { - if (isClosed) return; - emit( - SellPaymentInfoFailure( - PaymentInfoError.invalidAmountFormat, - message: e.toString(), - ), - ); - return; - } - final paymentInfo = await _sellPaymentInfoService.getPaymentInfo( - charged, + chargedFiatAmount(amount), iban, currency: currency, ); diff --git a/lib/screens/sell/widgets/sell_button.dart b/lib/screens/sell/widgets/sell_button.dart index 2bc3c5dec..81dcdca74 100644 --- a/lib/screens/sell/widgets/sell_button.dart +++ b/lib/screens/sell/widgets/sell_button.dart @@ -44,17 +44,6 @@ class SellButton extends StatelessWidget { } return; } - if (state.error == .invalidAmountFormat) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(S.of(context).invalidAmountFormatDescription), - backgroundColor: RealUnitColors.status.red600, - ), - ); - } - return; - } if (context.mounted && state.message.isNotEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/lib/widgets/fiat_input_formatter.dart b/lib/widgets/fiat_input_formatter.dart index 771178e93..997a4e9f8 100644 --- a/lib/widgets/fiat_input_formatter.dart +++ b/lib/widgets/fiat_input_formatter.dart @@ -16,12 +16,14 @@ class FiatInputFormatter extends TextInputFormatter { final normalized = normalizeFiatInput(allowed.text); if (normalized == allowed.text) return allowed; final cursor = allowed.selection.baseOffset.clamp(0, allowed.text.length); - final separatorsBeforeCursor = - RegExp('[.,]').allMatches(allowed.text.substring(0, cursor)).length; + // Mixed thousands + decimal (`1.000,50` → `1000,50`) drops only the + // thousands separator. Counting every [.,] before the cursor subtracted + // the kept decimal too and placed the caret one place too early. + final removed = allowed.text.length - normalized.length; return TextEditingValue( text: normalized, selection: TextSelection.collapsed( - offset: (cursor - separatorsBeforeCursor).clamp(0, normalized.length), + offset: (cursor - removed).clamp(0, normalized.length), ), ); } diff --git a/test/goldens/screens/buy/buy_golden_test.dart b/test/goldens/screens/buy/buy_golden_test.dart index 57554ce82..49b3185c5 100644 --- a/test/goldens/screens/buy/buy_golden_test.dart +++ b/test/goldens/screens/buy/buy_golden_test.dart @@ -281,12 +281,12 @@ void main() { when(() => converterCubit.state).thenReturn( const BuyConverterState( // This golden stubs the failure panel and does not run the - // formatter. Same-separator thousands groups (`1.000`, - // `1.000.000`) and mixed thousands + 1–2 decimal digits of the - // other separator (`1.000,50`, `1.300,75`) are stripped before - // the parser; a 3-digit mixed tail (`1.000,000`) still reaches - // chargedFiatAmount and throws FormatException. - fiatText: '1.300,75', + // formatter. Same-separator thousands groups and mixed + // thousands + 1–2 decimal digits of the other separator are + // stripped before the parser; a 3-digit mixed tail is not a + // unique thousands-then-decimal, so `1.000,000` is left as + // typed, reaches chargedFiatAmount, and throws FormatException. + fiatText: '1.000,000', sharesText: '', currency: Currency.chf, ), diff --git a/test/goldens/screens/sell/goldens/macos/sell_invalid_amount_format_snackbar.png b/test/goldens/screens/sell/goldens/macos/sell_invalid_amount_format_snackbar.png deleted file mode 100644 index 1ea51117a9ce303f3e0ed18ea7660e86d47f97e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28270 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 diff --git a/test/goldens/screens/sell/sell_states_golden_test.dart b/test/goldens/screens/sell/sell_states_golden_test.dart index 1a4f7a84e..f7b565644 100644 --- a/test/goldens/screens/sell/sell_states_golden_test.dart +++ b/test/goldens/screens/sell/sell_states_golden_test.dart @@ -14,7 +14,6 @@ import 'package:realunit_wallet/packages/service/dfx/dfx_brokerbot_service.dart' import 'package:realunit_wallet/packages/service/dfx/dfx_price_service.dart'; import 'package:realunit_wallet/packages/service/dfx/models/bank_account/bank_account.dart'; import 'package:realunit_wallet/packages/service/dfx/models/bank_account/dto/bank_account_dto.dart'; -import 'package:realunit_wallet/packages/service/dfx/models/payment/payment_info_error.dart'; import 'package:realunit_wallet/packages/service/dfx/models/payment/sell/dto/eip7702/eip7702_data_dto.dart'; import 'package:realunit_wallet/packages/service/dfx/models/payment/sell/dto/real_unit_sell_payment_info_dto.dart'; import 'package:realunit_wallet/packages/service/dfx/models/payment/sell/sell_payment_info.dart'; @@ -240,32 +239,6 @@ void main() { constraints: phoneConstraints, builder: () => wrapForGolden(buildSubject()), ); - - // SellButton's BlocConsumer shows the invalidAmountFormat SnackBar on the - // transition into that failure (sell_button.dart). Seed Initial and emit - // the failure so the listener fires; do not stub `state` to the final - // value — that would erase the transition. The single `pump` delivers the - // emission and `pumpAndSettle` runs the SnackBar entrance (the 4s - // auto-dismiss is a Timer, not a frame, so it stays visible). - goldenTest( - 'invalid amount format SnackBar', - fileName: 'sell_invalid_amount_format_snackbar', - constraints: phoneConstraints, - pumpBeforeTest: (tester) async { - await tester.pump(); - await tester.pumpAndSettle(); - }, - builder: () { - whenListen( - paymentInfoCubit, - Stream.value( - const SellPaymentInfoFailure(PaymentInfoError.invalidAmountFormat), - ), - initialState: const SellPaymentInfoInitial(), - ); - return wrapForGolden(buildSubject()); - }, - ); }); // ---- B) SellConfirmSheet (bottom sheet after payment-info success) ---- diff --git a/test/screens/sell/cubits/sell_payment_info_cubit_test.dart b/test/screens/sell/cubits/sell_payment_info_cubit_test.dart index df8989633..f41ed39a6 100644 --- a/test/screens/sell/cubits/sell_payment_info_cubit_test.dart +++ b/test/screens/sell/cubits/sell_payment_info_cubit_test.dart @@ -349,18 +349,6 @@ void main() { }, ); - test('grouping-ambiguous amount (1.000) → Failure(invalidAmountFormat), not unknown', () async { - final cubit = build(); - await cubit.getPaymentInfo(amount: '1.000', iban: 'CH56'); - - expect(cubit.state, isA()); - expect( - (cubit.state as SellPaymentInfoFailure).error, - PaymentInfoError.invalidAmountFormat, - ); - verifyNever(() => service.getPaymentInfo(any(), any(), currency: any(named: 'currency'))); - }); - test('FormatException from the service is unknown, not invalidAmountFormat', () async { when( () => service.getPaymentInfo(any(), any(), currency: any(named: 'currency')), diff --git a/test/screens/sell/sell_button_call_site_test.dart b/test/screens/sell/sell_button_call_site_test.dart index 81558f3cf..6ff6dfb0d 100644 --- a/test/screens/sell/sell_button_call_site_test.dart +++ b/test/screens/sell/sell_button_call_site_test.dart @@ -13,7 +13,6 @@ import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; import 'package:mocktail/mocktail.dart'; import 'package:realunit_wallet/generated/i18n.dart'; -import 'package:realunit_wallet/packages/service/dfx/models/payment/payment_info_error.dart'; import 'package:realunit_wallet/packages/service/dfx/models/payment/sell/dto/eip7702/eip7702_data_dto.dart'; import 'package:realunit_wallet/packages/service/dfx/models/payment/sell/dto/real_unit_sell_payment_info_dto.dart'; import 'package:realunit_wallet/packages/service/dfx/models/payment/sell/sell_payment_info.dart'; @@ -214,56 +213,5 @@ void main() { ); }, ); - - testWidgets( - 'shows a SnackBar with invalidAmountFormatDescription on invalidAmountFormat', - (tester) async { - whenListen( - sellPaymentInfoCubit, - Stream.fromIterable([ - const SellPaymentInfoFailure(PaymentInfoError.invalidAmountFormat), - ]), - initialState: const SellPaymentInfoInitial(), - ); - - final router = GoRouter( - routes: [ - GoRoute( - path: '/', - builder: (context, _) => Scaffold( - body: Center( - child: MultiBlocProvider( - providers: [ - BlocProvider.value(value: sellPaymentInfoCubit), - BlocProvider.value(value: sellConverterCubit), - ], - child: const SellButton(amount: '10', bankAccount: null), - ), - ), - ), - ), - ], - ); - addTearDown(router.dispose); - - await tester.pumpWidget( - MaterialApp.router( - theme: realUnitTheme, - routerConfig: router, - localizationsDelegates: const [ - S.delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ], - supportedLocales: S.delegate.supportedLocales, - ), - ); - await tester.pump(); - - expect(find.byType(SnackBar), findsOneWidget); - expect(find.text(S.current.invalidAmountFormatDescription), findsOneWidget); - }, - ); }); } diff --git a/test/widgets/fiat_input_formatter_test.dart b/test/widgets/fiat_input_formatter_test.dart index 98ea3dd90..cd912d020 100644 --- a/test/widgets/fiat_input_formatter_test.dart +++ b/test/widgets/fiat_input_formatter_test.dart @@ -42,6 +42,7 @@ void main() { ), ); expect(out.text, '1000,50'); + expect(out.selection, const TextSelection.collapsed(offset: 7)); }); test('rewrites a one-step paste of 1,000.50 to 1000.50', () { @@ -53,6 +54,29 @@ void main() { ), ); expect(out.text, '1000.50'); + expect(out.selection, const TextSelection.collapsed(offset: 7)); + }); + + test('a digit typed after a mixed thousands-and-decimal paste lands at the end', () { + final pasted = update( + TextEditingValue.empty, + const TextEditingValue( + text: '1.000,5', + selection: TextSelection.collapsed(offset: 7), + ), + ); + expect(pasted.text, '1000,5'); + expect(pasted.selection, const TextSelection.collapsed(offset: 6)); + + final typed = update( + pasted, + TextEditingValue( + text: '${pasted.text}9', + selection: TextSelection.collapsed(offset: pasted.selection.baseOffset + 1), + ), + ); + expect(typed.text, '1000,59'); + expect(typed.selection, const TextSelection.collapsed(offset: 7)); }); test('leaves a one-step paste of grouping-ambiguous 1.000,000 unchanged', () { From 87aa017b7b66e58400687c8f28a385c794374415 Mon Sep 17 00:00:00 2001 From: joshuakrueger-dfx Date: Thu, 20 Aug 2026 15:37:06 +0200 Subject: [PATCH 17/18] fix(input): count only the separators dropped before the caret MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous caret fix used the global length delta, which also subtracts separators sitting behind the caret. Typing a digit at the start of .000,50 moved the caret to 0 instead of 1 — the next keystroke would land inside the number. The formatter now asks which separator gets stripped and counts only its occurrences left of the caret. --- lib/packages/utils/fiat_amount.dart | 20 +++++++++++-- lib/widgets/fiat_input_formatter.dart | 16 ++++++---- test/widgets/fiat_input_formatter_test.dart | 33 +++++++++++++++++++++ 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/lib/packages/utils/fiat_amount.dart b/lib/packages/utils/fiat_amount.dart index 30e4599ab..9d41e0b3c 100644 --- a/lib/packages/utils/fiat_amount.dart +++ b/lib/packages/utils/fiat_amount.dart @@ -7,6 +7,12 @@ double? tryParseFiatAmount(String input) { return double.tryParse(input.replaceAll(',', '.')); } +/// Consecutive groups of three digits with the same separator. +final _sameGrouping = RegExp(r'^(\d+)([.,])(\d{3}(?:\2\d{3})*)$'); + +/// Thousands groups plus 1–2 digits of the other separator. +final _mixedGrouping = RegExp(r'^(\d+)([.,])(\d{3}(?:\2\d{3})*)([.,])(\d{1,2})$'); + /// Strips thousands grouping (`1.000` / `1,000` / `1.000.000` → integer). /// /// EUR and CHF have two decimal places, so a separator followed by exactly @@ -19,16 +25,26 @@ double? tryParseFiatAmount(String input) { /// `1.00`) are returned unchanged, as is mixed input that is not uniquely /// thousands-then-decimal (`1.000,000`, `3,5,7`). String normalizeFiatInput(String input) { - if (RegExp(r'^(\d+)([.,])(\d{3}(?:\2\d{3})*)$').hasMatch(input)) { + if (_sameGrouping.hasMatch(input)) { return input.replaceAll('.', '').replaceAll(',', ''); } - final mixed = RegExp(r'^(\d+)([.,])(\d{3}(?:\2\d{3})*)([.,])(\d{1,2})$').firstMatch(input); + final mixed = _mixedGrouping.firstMatch(input); if (mixed != null && mixed[2] != mixed[4]) { return input.replaceAll(mixed[2]!, ''); } return input; } +/// The grouping separator [normalizeFiatInput] strips from [input], or null +/// if [input] is left unchanged. +String? strippedFiatGroupingSeparator(String input) { + final same = _sameGrouping.firstMatch(input); + if (same != null) return same[2]!; + final mixed = _mixedGrouping.firstMatch(input); + if (mixed != null && mixed[2] != mixed[4]) return mixed[2]!; + return null; +} + /// The whole-currency integer the backend charges for the raw [input] the user /// typed (e.g. `300,75` → `301`); empty input counts as zero. int chargedFiatAmount(String input) { diff --git a/lib/widgets/fiat_input_formatter.dart b/lib/widgets/fiat_input_formatter.dart index 997a4e9f8..482428cdc 100644 --- a/lib/widgets/fiat_input_formatter.dart +++ b/lib/widgets/fiat_input_formatter.dart @@ -13,17 +13,23 @@ class FiatInputFormatter extends TextInputFormatter { @override TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { final allowed = _allowedChars.formatEditUpdate(oldValue, newValue); + final separator = strippedFiatGroupingSeparator(allowed.text); final normalized = normalizeFiatInput(allowed.text); if (normalized == allowed.text) return allowed; final cursor = allowed.selection.baseOffset.clamp(0, allowed.text.length); - // Mixed thousands + decimal (`1.000,50` → `1000,50`) drops only the - // thousands separator. Counting every [.,] before the cursor subtracted - // the kept decimal too and placed the caret one place too early. - final removed = allowed.text.length - normalized.length; + // Only separators actually dropped that sit before the caret. A global + // length delta also subtracts separators behind it (typing `1` into + // `.000,50` would move the caret to 0 instead of 1). + var removedBefore = 0; + if (separator != null) { + for (var i = 0; i < cursor; i++) { + if (allowed.text[i] == separator) removedBefore++; + } + } return TextEditingValue( text: normalized, selection: TextSelection.collapsed( - offset: (cursor - removed).clamp(0, normalized.length), + offset: (cursor - removedBefore).clamp(0, normalized.length), ), ); } diff --git a/test/widgets/fiat_input_formatter_test.dart b/test/widgets/fiat_input_formatter_test.dart index cd912d020..9a04d546b 100644 --- a/test/widgets/fiat_input_formatter_test.dart +++ b/test/widgets/fiat_input_formatter_test.dart @@ -121,5 +121,38 @@ void main() { expect(out.text, '1000'); expect(out.selection, const TextSelection.collapsed(offset: 1)); }); + + test('places the caret after a digit typed at the start of .000,50', () { + // `.000,50` is a fixpoint (both regexes require a leading `\d+`). Typing + // `1` at offset 0 yields `1.000,50` → `1000,50`. The stripped `.` sits + // behind the caret, so a global length delta would move it to 0. + final out = update( + const TextEditingValue( + text: '.000,50', + selection: TextSelection.collapsed(offset: 0), + ), + const TextEditingValue( + text: '1.000,50', + selection: TextSelection.collapsed(offset: 1), + ), + ); + expect(out.text, '1000,50'); + expect(out.selection, const TextSelection.collapsed(offset: 1)); + }); + + test('places the caret after a digit typed at the start of .000.000', () { + final out = update( + const TextEditingValue( + text: '.000.000', + selection: TextSelection.collapsed(offset: 0), + ), + const TextEditingValue( + text: '1.000.000', + selection: TextSelection.collapsed(offset: 1), + ), + ); + expect(out.text, '1000000'); + expect(out.selection, const TextSelection.collapsed(offset: 1)); + }); }); } From d9e0914df731809568be014d7349b2483121898f Mon Sep 17 00:00:00 2001 From: joshuakrueger-dfx Date: Thu, 20 Aug 2026 16:14:36 +0200 Subject: [PATCH 18/18] test(input): pin which separator the caret math removes Every mixed-separator test placed the caret where both separators appear equally often before it, so returning the decimal separator instead of the thousands one passed the whole suite. Two cases now type into 1.00,50 and 1,00.50 where the counts differ. The same/mixed decision also lived twice, once in each public function; it is derived once now. The repro stub floor and its comment agreed on different numbers and now name the same one. --- lib/packages/utils/fiat_amount.dart | 26 +++++++-------- .../buy/buy_amount_change_repro_test.dart | 9 +++--- test/widgets/fiat_input_formatter_test.dart | 32 +++++++++++++++++++ 3 files changed, 50 insertions(+), 17 deletions(-) diff --git a/lib/packages/utils/fiat_amount.dart b/lib/packages/utils/fiat_amount.dart index 9d41e0b3c..4070938f3 100644 --- a/lib/packages/utils/fiat_amount.dart +++ b/lib/packages/utils/fiat_amount.dart @@ -13,6 +13,15 @@ final _sameGrouping = RegExp(r'^(\d+)([.,])(\d{3}(?:\2\d{3})*)$'); /// Thousands groups plus 1–2 digits of the other separator. final _mixedGrouping = RegExp(r'^(\d+)([.,])(\d{3}(?:\2\d{3})*)([.,])(\d{1,2})$'); +/// Grouping separator to drop from [input], or null if [input] is unchanged. +String? _groupingSeparatorToStrip(String input) { + final same = _sameGrouping.firstMatch(input); + if (same != null) return same[2]!; + final mixed = _mixedGrouping.firstMatch(input); + if (mixed != null && mixed[2] != mixed[4]) return mixed[2]!; + return null; +} + /// Strips thousands grouping (`1.000` / `1,000` / `1.000.000` → integer). /// /// EUR and CHF have two decimal places, so a separator followed by exactly @@ -25,24 +34,15 @@ final _mixedGrouping = RegExp(r'^(\d+)([.,])(\d{3}(?:\2\d{3})*)([.,])(\d{1,2})$' /// `1.00`) are returned unchanged, as is mixed input that is not uniquely /// thousands-then-decimal (`1.000,000`, `3,5,7`). String normalizeFiatInput(String input) { - if (_sameGrouping.hasMatch(input)) { - return input.replaceAll('.', '').replaceAll(',', ''); - } - final mixed = _mixedGrouping.firstMatch(input); - if (mixed != null && mixed[2] != mixed[4]) { - return input.replaceAll(mixed[2]!, ''); - } - return input; + final separator = _groupingSeparatorToStrip(input); + if (separator == null) return input; + return input.replaceAll(separator, ''); } /// The grouping separator [normalizeFiatInput] strips from [input], or null /// if [input] is left unchanged. String? strippedFiatGroupingSeparator(String input) { - final same = _sameGrouping.firstMatch(input); - if (same != null) return same[2]!; - final mixed = _mixedGrouping.firstMatch(input); - if (mixed != null && mixed[2] != mixed[4]) return mixed[2]!; - return null; + return _groupingSeparatorToStrip(input); } /// The whole-currency integer the backend charges for the raw [input] the user diff --git a/test/screens/buy/buy_amount_change_repro_test.dart b/test/screens/buy/buy_amount_change_repro_test.dart index 7aec98189..3c07d5b1f 100644 --- a/test/screens/buy/buy_amount_change_repro_test.dart +++ b/test/screens/buy/buy_amount_change_repro_test.dart @@ -44,7 +44,8 @@ class MockSupportedFiatRepository extends Mock implements SupportedFiatRepositor /// timer body (brokerbot call → loading:false → payment-info fetch) can finish. const _afterDebounce = Duration(milliseconds: 150); -/// Production-like floor used when the quote returns `AmountTooLow`. +/// Production-like floor: quotes below this return `AmountTooLow`, and the +/// error payload reports the same value as `minVolume`. const _minVolume = 100.0; void main() { @@ -97,8 +98,8 @@ void main() { expectedAmount: '100', reasonPrefix: 'After deleting 300 digit-by-digit and typing 100, the screen must ' - 'keep a valid quote. Intermediate 0–3 are AmountTooLow only; 100 ' - 'is a valid quote in production.', + 'keep a valid quote. Intermediate amounts below the production ' + 'floor of 100 are AmountTooLow; 100 is a valid quote.', ); }, ); @@ -706,7 +707,7 @@ void _stubProductionLikeQuotes( final amount = invocation.positionalArguments[0] as int; final currency = invocation.namedArguments[#currency] as Currency? ?? Currency.chf; - if (amount >= 4) { + if (amount >= _minVolume) { return _quote(amount: amount, currency: currency, isValid: true); } return _quote( diff --git a/test/widgets/fiat_input_formatter_test.dart b/test/widgets/fiat_input_formatter_test.dart index 9a04d546b..a588ee82e 100644 --- a/test/widgets/fiat_input_formatter_test.dart +++ b/test/widgets/fiat_input_formatter_test.dart @@ -122,6 +122,38 @@ void main() { expect(out.selection, const TextSelection.collapsed(offset: 1)); }); + test('places the caret after a 0 typed into 1.00,50 before the decimal', () { + // `1.00|`,50 plus `0` → `1.000|,50`. One `.` and zero `,` sit before + // the caret; counting the decimal comma instead would leave it at 5. + final out = update( + const TextEditingValue( + text: '1.00,50', + selection: TextSelection.collapsed(offset: 4), + ), + const TextEditingValue( + text: '1.000,50', + selection: TextSelection.collapsed(offset: 5), + ), + ); + expect(out.text, '1000,50'); + expect(out.selection, const TextSelection.collapsed(offset: 4)); + }); + + test('places the caret after a 0 typed into 1,00.50 before the decimal', () { + final out = update( + const TextEditingValue( + text: '1,00.50', + selection: TextSelection.collapsed(offset: 4), + ), + const TextEditingValue( + text: '1,000.50', + selection: TextSelection.collapsed(offset: 5), + ), + ); + expect(out.text, '1000.50'); + expect(out.selection, const TextSelection.collapsed(offset: 4)); + }); + test('places the caret after a digit typed at the start of .000,50', () { // `.000,50` is a fixpoint (both regexes require a leading `\d+`). Typing // `1` at offset 0 yields `1.000,50` → `1000,50`. The stripped `.` sits