From 79881607e7a4167a62d3d8e520337341032fd4e5 Mon Sep 17 00:00:00 2001 From: Francois Date: Fri, 24 Oct 2025 02:02:21 +0200 Subject: [PATCH 1/9] fix(auth): show seed backup banner in mobile view twas disabled by default for mobile for some reason (undocumented) --- lib/bloc/taker_form/taker_bloc.dart | 93 +++++++++++++------------ lib/views/common/pages/page_layout.dart | 16 ++--- 2 files changed, 53 insertions(+), 56 deletions(-) diff --git a/lib/bloc/taker_form/taker_bloc.dart b/lib/bloc/taker_form/taker_bloc.dart index 727a70bde5..90b9a57803 100644 --- a/lib/bloc/taker_form/taker_bloc.dart +++ b/lib/bloc/taker_form/taker_bloc.dart @@ -3,14 +3,17 @@ import 'dart:async'; import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:logging/logging.dart'; import 'package:rational/rational.dart'; +import 'package:web_dex/analytics/events/transaction_events.dart'; import 'package:web_dex/app_config/app_config.dart'; +import 'package:web_dex/bloc/analytics/analytics_bloc.dart'; import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/dex_repository.dart'; import 'package:web_dex/bloc/taker_form/taker_event.dart'; import 'package:web_dex/bloc/taker_form/taker_state.dart'; -import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:web_dex/bloc/taker_form/taker_validator.dart'; import 'package:web_dex/bloc/transformers.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; @@ -24,11 +27,9 @@ import 'package:web_dex/model/data_from_service.dart'; import 'package:web_dex/model/dex_form_error.dart'; import 'package:web_dex/model/text_error.dart'; import 'package:web_dex/model/trade_preimage.dart'; +import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/views/dex/dex_helpers.dart'; -import 'package:web_dex/bloc/analytics/analytics_bloc.dart'; -import 'package:web_dex/analytics/events/transaction_events.dart'; -import 'package:web_dex/model/wallet.dart'; class TakerBloc extends Bloc { TakerBloc({ @@ -99,6 +100,7 @@ class TakerBloc extends Bloc { bool _isLoggedIn = false; late TakerValidator _validator; late StreamSubscription _authorizationSubscription; + final Logger _log = Logger('TakerBloc'); Future _onStartSwap( TakerStartSwap event, @@ -123,8 +125,7 @@ class TakerBloc extends Bloc { // Log swap failure analytics event for immediate RPC errors final walletType = - (await _sdk.auth.currentUser)?.wallet.config.type.name ?? - 'unknown'; + (await _sdk.auth.currentUser)?.wallet.config.type.name ?? 'unknown'; _analyticsBloc.logEvent( SwapFailedEventData( asset: state.sellCoin!.abbr, @@ -447,53 +448,57 @@ class TakerBloc extends Bloc { ); } - // Required here because of the manual RPC calls that bypass the sdk - final activeAssets = await _sdk.assets.getActivatedAssets(); - final isAssetActive = activeAssets.any( - (asset) => asset.id == state.sellCoin!.id, - ); - if (!isAssetActive) { - // Intentionally leave the state as loading so that a spinner is shown - // instead of a "0.00" balance hinting that the asset is active when it - // is not. - if (state.availableBalanceState != AvailableBalanceState.loading) { - emitter( - state.copyWith( - availableBalanceState: () => AvailableBalanceState.loading, - ), - ); + try { + // Required here because of the manual RPC calls that bypass the sdk + final activeAssets = await _sdk.assets.getActivatedAssets(); + final isAssetActive = activeAssets.any( + (asset) => asset.id == state.sellCoin!.id, + ); + if (!isAssetActive) { + // Intentionally leave the state as loading so that a spinner is shown + // instead of a "0.00" balance hinting that the asset is active when it + // is not. + if (state.availableBalanceState != AvailableBalanceState.loading) { + emitter( + state.copyWith( + availableBalanceState: () => AvailableBalanceState.loading, + ), + ); + } + return; } - return; - } - if (!_isLoggedIn) { - emitter( - state.copyWith( - availableBalanceState: () => AvailableBalanceState.unavailable, - ), - ); - } else { - Rational? maxSellAmount = await _dexRepo.getMaxTakerVolume( - state.sellCoin!.abbr, - ); - if (maxSellAmount != null) { + if (!_isLoggedIn) { emitter( state.copyWith( - maxSellAmount: () => maxSellAmount, - availableBalanceState: () => AvailableBalanceState.success, + availableBalanceState: () => AvailableBalanceState.unavailable, ), ); } else { - maxSellAmount = await _frequentlyGetMaxTakerVolume(); - emitter( - state.copyWith( - maxSellAmount: () => maxSellAmount, - availableBalanceState: maxSellAmount == null - ? () => AvailableBalanceState.failure - : () => AvailableBalanceState.success, - ), + Rational? maxSellAmount = await _dexRepo.getMaxTakerVolume( + state.sellCoin!.abbr, ); + if (maxSellAmount != null) { + emitter( + state.copyWith( + maxSellAmount: () => maxSellAmount, + availableBalanceState: () => AvailableBalanceState.success, + ), + ); + } else { + maxSellAmount = await _frequentlyGetMaxTakerVolume(); + emitter( + state.copyWith( + maxSellAmount: () => maxSellAmount, + availableBalanceState: maxSellAmount == null + ? () => AvailableBalanceState.failure + : () => AvailableBalanceState.success, + ), + ); + } } + } catch (e, s) { + _log.severe('Failed to update max sell amount', e, s); } } diff --git a/lib/views/common/pages/page_layout.dart b/lib/views/common/pages/page_layout.dart index 5347c6d3bc..9142f9e9de 100644 --- a/lib/views/common/pages/page_layout.dart +++ b/lib/views/common/pages/page_layout.dart @@ -53,20 +53,15 @@ class _MobileLayout extends StatelessWidget { return Column( mainAxisSize: MainAxisSize.max, children: [ - const BackupSeedNotification(), + const BackupSeedNotification(hideOnMobile: false), if (header != null) header!, Flexible( child: PagePlate( noBackground: noBackground, padding: padding, - child: Column( - mainAxisSize: MainAxisSize.max, - children: [ - content, - ], - ), + child: Column(mainAxisSize: MainAxisSize.max, children: [content]), ), - ) + ), ], ); } @@ -97,10 +92,7 @@ class _DesktopLayout extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (header != null) ...[ - const SizedBox(height: 23), - header!, - ], + if (header != null) ...[const SizedBox(height: 23), header!], content, ], ), From 44ca2fa9fd3bf06025479a9c9cd59b85c136aa7a Mon Sep 17 00:00:00 2001 From: Francois Date: Fri, 24 Oct 2025 04:14:56 +0200 Subject: [PATCH 2/9] fix: dismiss keyboard on scroll for fiat and swap inputs --- .../simple/form/maker/maker_form_layout.dart | 34 +++- .../simple/form/taker/taker_form_layout.dart | 84 ++++++-- lib/views/fiat/custom_fiat_input_field.dart | 69 +++++-- lib/views/fiat/fiat_form.dart | 192 ++++++++++-------- lib/views/fiat/fiat_inputs.dart | 41 ++-- lib/views/fiat/fiat_select_button.dart | 50 ++--- 6 files changed, 310 insertions(+), 160 deletions(-) diff --git a/lib/views/dex/simple/form/maker/maker_form_layout.dart b/lib/views/dex/simple/form/maker/maker_form_layout.dart index 5111f33bef..7f71709025 100644 --- a/lib/views/dex/simple/form/maker/maker_form_layout.dart +++ b/lib/views/dex/simple/form/maker/maker_form_layout.dart @@ -39,8 +39,9 @@ class _MakerFormLayoutState extends State { if (routingState.dexState.orderType != 'taker') { if (routingState.dexState.fromCurrency.isNotEmpty) { - final Coin? sellCoin = - coinsRepository.getCoin(routingState.dexState.fromCurrency); + final Coin? sellCoin = coinsRepository.getCoin( + routingState.dexState.fromCurrency, + ); if (sellCoin != null) { makerFormBloc.sellCoin = sellCoin; @@ -52,8 +53,9 @@ class _MakerFormLayoutState extends State { } if (routingState.dexState.toCurrency.isNotEmpty) { - final Coin? buyCoin = - coinsRepository.getCoin(routingState.dexState.toCurrency); + final Coin? buyCoin = coinsRepository.getCoin( + routingState.dexState.toCurrency, + ); if (buyCoin != null) { makerFormBloc.buyCoin = buyCoin; @@ -105,7 +107,8 @@ class _MakerFormDesktopLayout extends StatefulWidget { const _MakerFormDesktopLayout(); @override - State<_MakerFormDesktopLayout> createState() => _MakerFormDesktopLayoutState(); + State<_MakerFormDesktopLayout> createState() => + _MakerFormDesktopLayoutState(); } class _MakerFormDesktopLayoutState extends State<_MakerFormDesktopLayout> { @@ -117,15 +120,24 @@ class _MakerFormDesktopLayoutState extends State<_MakerFormDesktopLayout> { super.initState(); _mainScrollController = ScrollController(); _orderbookScrollController = ScrollController(); + _mainScrollController.addListener(_onScroll); + _orderbookScrollController.addListener(_onScroll); } @override void dispose() { + _mainScrollController.removeListener(_onScroll); + _orderbookScrollController.removeListener(_onScroll); _mainScrollController.dispose(); _orderbookScrollController.dispose(); super.dispose(); } + void _onScroll() { + // Dismiss keyboard when user starts scrolling + FocusScope.of(context).unfocus(); + } + @override Widget build(BuildContext context) { return Row( @@ -146,8 +158,9 @@ class _MakerFormDesktopLayoutState extends State<_MakerFormDesktopLayout> { key: const Key('maker-form-layout-scroll'), controller: _mainScrollController, child: ConstrainedBox( - constraints: - BoxConstraints(maxWidth: theme.custom.dexFormWidth), + constraints: BoxConstraints( + maxWidth: theme.custom.dexFormWidth, + ), child: const Stack( clipBehavior: Clip.none, children: [ @@ -189,14 +202,21 @@ class _MakerFormMobileLayoutState extends State<_MakerFormMobileLayout> { void initState() { super.initState(); _scrollController = ScrollController(); + _scrollController.addListener(_onScroll); } @override void dispose() { + _scrollController.removeListener(_onScroll); _scrollController.dispose(); super.dispose(); } + void _onScroll() { + // Dismiss keyboard when user starts scrolling + FocusScope.of(context).unfocus(); + } + @override Widget build(BuildContext context) { return SingleChildScrollView( diff --git a/lib/views/dex/simple/form/taker/taker_form_layout.dart b/lib/views/dex/simple/form/taker/taker_form_layout.dart index 89714a04d1..d3381a76a2 100644 --- a/lib/views/dex/simple/form/taker/taker_form_layout.dart +++ b/lib/views/dex/simple/form/taker/taker_form_layout.dart @@ -22,17 +22,48 @@ class TakerFormLayout extends StatelessWidget { return step == TakerStep.confirm ? const TakerOrderConfirmation() : isMobile - ? const _TakerFormMobileLayout() - : _TakerFormDesktopLayout(); + ? const _TakerFormMobileLayout() + : _TakerFormDesktopLayout(); }, ); } } -class _TakerFormDesktopLayout extends StatelessWidget { +class _TakerFormDesktopLayout extends StatefulWidget { + @override + State<_TakerFormDesktopLayout> createState() => + _TakerFormDesktopLayoutState(); +} + +class _TakerFormDesktopLayoutState extends State<_TakerFormDesktopLayout> { + late final ScrollController _mainScrollController; + late final ScrollController _orderbookScrollController; + + @override + void initState() { + super.initState(); + _mainScrollController = ScrollController(); + _orderbookScrollController = ScrollController(); + _mainScrollController.addListener(_onScroll); + _orderbookScrollController.addListener(_onScroll); + } + + @override + void dispose() { + _mainScrollController.removeListener(_onScroll); + _orderbookScrollController.removeListener(_onScroll); + _mainScrollController.dispose(); + _orderbookScrollController.dispose(); + super.dispose(); + } + + void _onScroll() { + // Dismiss keyboard when user starts scrolling + FocusScope.of(context).unfocus(); + } + @override Widget build(BuildContext context) { - final scrollController = ScrollController(); return Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, @@ -45,14 +76,15 @@ class _TakerFormDesktopLayout extends StatelessWidget { Flexible( flex: 6, child: DexScrollbar( - scrollController: scrollController, + scrollController: _mainScrollController, isMobile: isMobile, child: SingleChildScrollView( key: const Key('taker-form-layout-scroll'), - controller: scrollController, + controller: _mainScrollController, child: ConstrainedBox( - constraints: - BoxConstraints(maxWidth: theme.custom.dexFormWidth), + constraints: BoxConstraints( + maxWidth: theme.custom.dexFormWidth, + ), child: Stack( clipBehavior: Clip.none, children: [ @@ -76,21 +108,49 @@ class _TakerFormDesktopLayout extends StatelessWidget { child: Padding( padding: const EdgeInsets.only(left: 20), child: SingleChildScrollView( - controller: ScrollController(), child: const TakerOrderbook()), + controller: _orderbookScrollController, + child: const TakerOrderbook(), + ), ), - ) + ), ], ); } } -class _TakerFormMobileLayout extends StatelessWidget { +class _TakerFormMobileLayout extends StatefulWidget { const _TakerFormMobileLayout(); + @override + State<_TakerFormMobileLayout> createState() => _TakerFormMobileLayoutState(); +} + +class _TakerFormMobileLayoutState extends State<_TakerFormMobileLayout> { + late final ScrollController _scrollController; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + _scrollController.addListener(_onScroll); + } + + @override + void dispose() { + _scrollController.removeListener(_onScroll); + _scrollController.dispose(); + super.dispose(); + } + + void _onScroll() { + // Dismiss keyboard when user starts scrolling + FocusScope.of(context).unfocus(); + } + @override Widget build(BuildContext context) { return SingleChildScrollView( - controller: ScrollController(), + controller: _scrollController, child: ConstrainedBox( constraints: BoxConstraints(maxWidth: theme.custom.dexFormWidth), child: Stack( diff --git a/lib/views/fiat/custom_fiat_input_field.dart b/lib/views/fiat/custom_fiat_input_field.dart index ec25d580b6..b73fd967f4 100644 --- a/lib/views/fiat/custom_fiat_input_field.dart +++ b/lib/views/fiat/custom_fiat_input_field.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import 'package:web_dex/shared/constants.dart'; import 'package:web_dex/shared/utils/formatters.dart'; -class CustomFiatInputField extends StatelessWidget { +class CustomFiatInputField extends StatefulWidget { const CustomFiatInputField({ required this.controller, required this.hintText, @@ -13,6 +13,8 @@ class CustomFiatInputField extends StatelessWidget { this.label, this.readOnly = false, this.inputError, + this.focusNode, + this.onSubmitted, }); final TextEditingController controller; @@ -22,27 +24,51 @@ class CustomFiatInputField extends StatelessWidget { final bool readOnly; final Widget assetButton; final String? inputError; + final FocusNode? focusNode; + final void Function(String)? onSubmitted; + + @override + State createState() => _CustomFiatInputFieldState(); +} + +class _CustomFiatInputFieldState extends State { + late FocusNode _focusNode; + + @override + void initState() { + super.initState(); + _focusNode = widget.focusNode ?? FocusNode(); + } + + @override + void dispose() { + if (widget.focusNode == null) { + _focusNode.dispose(); + } + super.dispose(); + } @override Widget build(BuildContext context) { final textColor = Theme.of(context).colorScheme.onSurfaceVariant; final inputStyle = Theme.of(context).textTheme.headlineLarge?.copyWith( - fontSize: 18, - fontWeight: FontWeight.w300, - color: textColor, - letterSpacing: 1.1, - ); + fontSize: 18, + fontWeight: FontWeight.w300, + color: textColor, + letterSpacing: 1.1, + ); final InputDecoration inputDecoration = InputDecoration( - label: label, + label: widget.label, labelStyle: inputStyle, fillColor: Theme.of(context).colorScheme.onSurface, - floatingLabelStyle: - Theme.of(context).inputDecorationTheme.floatingLabelStyle, + floatingLabelStyle: Theme.of( + context, + ).inputDecorationTheme.floatingLabelStyle, floatingLabelBehavior: FloatingLabelBehavior.always, contentPadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), - hintText: hintText, + hintText: widget.hintText, border: const OutlineInputBorder( borderRadius: BorderRadius.only( bottomLeft: Radius.circular(4), @@ -51,7 +77,7 @@ class CustomFiatInputField extends StatelessWidget { topRight: Radius.circular(18), ), ), - errorText: inputError, + errorText: widget.inputError, errorMaxLines: 1, helperText: '', ); @@ -61,24 +87,25 @@ class CustomFiatInputField extends StatelessWidget { alignment: Alignment.centerRight, children: [ TextField( - autofocus: true, - controller: controller, + autofocus: false, + controller: widget.controller, + focusNode: _focusNode, style: inputStyle, decoration: inputDecoration, - readOnly: readOnly, - onChanged: onTextChanged, + readOnly: widget.readOnly, + onChanged: widget.onTextChanged, + onSubmitted: (value) { + _focusNode.unfocus(); + widget.onSubmitted?.call(value); + }, inputFormatters: [ FilteringTextInputFormatter.allow(numberRegExp), DecimalTextInputFormatter(decimalRange: 2), ], keyboardType: const TextInputType.numberWithOptions(decimal: true), + textInputAction: TextInputAction.done, ), - Positioned( - right: 16, - bottom: 26, - top: 2, - child: assetButton, - ), + Positioned(right: 16, bottom: 26, top: 2, child: widget.assetButton), ], ); } diff --git a/lib/views/fiat/fiat_form.dart b/lib/views/fiat/fiat_form.dart index 54ac76175f..af16121690 100644 --- a/lib/views/fiat/fiat_form.dart +++ b/lib/views/fiat/fiat_form.dart @@ -32,6 +32,7 @@ class FiatForm extends StatefulWidget { class _FiatFormState extends State { bool _isLoggedIn = false; + late ScrollController _scrollController; @override void initState() { @@ -39,6 +40,10 @@ class _FiatFormState extends State { _isLoggedIn = RepositoryProvider.of(context).state.isSignedIn; + // Initialize scroll controller and add listener for keyboard dismissal + _scrollController = ScrollController(); + _scrollController.addListener(_onScroll); + final fiatFormBloc = context.read() ..add(const FiatFormCurrenciesRefreshRequested()) ..add(const FiatFormPaymentMethodsRefreshRequested()); @@ -52,6 +57,18 @@ class _FiatFormState extends State { } } + @override + void dispose() { + _scrollController.removeListener(_onScroll); + _scrollController.dispose(); + super.dispose(); + } + + void _onScroll() { + // Dismiss keyboard when user starts scrolling + FocusScope.of(context).unfocus(); + } + @override Widget build(BuildContext context) { // TODO: Add optimisations to re-use the generated checkout URL if the user @@ -67,7 +84,6 @@ class _FiatFormState extends State { // creating the checkout URL. This could clutter up the order history with // orders that were never completed. - final scrollController = ScrollController(); return BlocListener( listener: (context, state) { _handleAccountStatusChange(state.isSignedIn); @@ -76,80 +92,86 @@ class _FiatFormState extends State { listenWhen: (previous, current) => previous.fiatOrderStatus != current.fiatOrderStatus, listener: (context, state) => _handlePaymentStatusUpdate(state), - builder: (context, state) => DexScrollbar( - isMobile: isMobile, - scrollController: scrollController, - child: SingleChildScrollView( - key: const Key('fiat-form-scroll'), - controller: scrollController, - child: Column( - children: [ - FiatActionTabBar( - currentTabIndex: state.fiatMode.tabIndex, - onTabClick: _setActiveTab, - ), - const SizedBox(height: 16), - if (state.fiatMode == FiatMode.offramp) - Center(child: Text(LocaleKeys.comingSoon.tr())) - else - GradientBorder( - innerColor: dexPageColors.frontPlate, - gradient: dexPageColors.formPlateGradient, - child: Container( - padding: const EdgeInsets.fromLTRB(16, 32, 16, 16), - child: Column( - children: [ - FiatInputs( - onFiatCurrencyChanged: _onFiatChanged, - onCoinChanged: _onCoinChanged, - onFiatAmountUpdate: _onFiatAmountChanged, - onSourceAddressChanged: _onSourceAddressChanged, - initialFiat: state.selectedFiat.value!, - selectedAsset: state.selectedAsset.value!, - selectedAssetAddress: state.selectedAssetAddress, - selectedAssetPubkeys: state.selectedCoinPubkeys, - initialFiatAmount: state.fiatAmount.valueAsDecimal, - fiatList: state.fiatList, - coinList: state.coinList, - selectedPaymentMethodPrice: - state.selectedPaymentMethod.priceInfo, - isLoggedIn: _isLoggedIn, - fiatMinAmount: state.minFiatAmount, - fiatMaxAmount: state.maxFiatAmount, - boundariesError: - state.fiatAmount.error?.text(state), - ), - const SizedBox(height: 16), - FiatPaymentMethodsGrid(state: state), - const SizedBox(height: 16), - ConnectWalletWrapper( - key: const Key('connect-wallet-fiat-form'), - eventType: WalletsManagerEventType.fiat, - child: UiPrimaryButton( - key: const Key('fiat-onramp-submit-button'), - height: 40, - text: state.fiatOrderStatus.isSubmitting - ? '${LocaleKeys.submitting.tr()}...' - : LocaleKeys.buyNow.tr(), - onPressed: - state.canSubmit ? _completeOrder : null, + builder: (context, state) => GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: DexScrollbar( + isMobile: isMobile, + scrollController: _scrollController, + child: SingleChildScrollView( + key: const Key('fiat-form-scroll'), + controller: _scrollController, + child: Column( + children: [ + FiatActionTabBar( + currentTabIndex: state.fiatMode.tabIndex, + onTabClick: _setActiveTab, + ), + const SizedBox(height: 16), + if (state.fiatMode == FiatMode.offramp) + Center(child: Text(LocaleKeys.comingSoon.tr())) + else + GradientBorder( + innerColor: dexPageColors.frontPlate, + gradient: dexPageColors.formPlateGradient, + child: Container( + padding: const EdgeInsets.fromLTRB(16, 32, 16, 16), + child: Column( + children: [ + FiatInputs( + onFiatCurrencyChanged: _onFiatChanged, + onCoinChanged: _onCoinChanged, + onFiatAmountUpdate: _onFiatAmountChanged, + onSourceAddressChanged: _onSourceAddressChanged, + initialFiat: state.selectedFiat.value!, + selectedAsset: state.selectedAsset.value!, + selectedAssetAddress: state.selectedAssetAddress, + selectedAssetPubkeys: state.selectedCoinPubkeys, + initialFiatAmount: + state.fiatAmount.valueAsDecimal, + fiatList: state.fiatList, + coinList: state.coinList, + selectedPaymentMethodPrice: + state.selectedPaymentMethod.priceInfo, + isLoggedIn: _isLoggedIn, + fiatMinAmount: state.minFiatAmount, + fiatMaxAmount: state.maxFiatAmount, + boundariesError: state.fiatAmount.error?.text( + state, + ), ), - ), - const SizedBox(height: 16), - Text( - _isLoggedIn - ? state.fiatOrderStatus.isFailed - ? LocaleKeys.fiatCantCompleteOrder.tr() - : LocaleKeys.fiatPriceCanChange.tr() - : LocaleKeys.fiatConnectWallet.tr(), - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodySmall, - ), - ], + const SizedBox(height: 16), + FiatPaymentMethodsGrid(state: state), + const SizedBox(height: 16), + ConnectWalletWrapper( + key: const Key('connect-wallet-fiat-form'), + eventType: WalletsManagerEventType.fiat, + child: UiPrimaryButton( + key: const Key('fiat-onramp-submit-button'), + height: 40, + text: state.fiatOrderStatus.isSubmitting + ? '${LocaleKeys.submitting.tr()}...' + : LocaleKeys.buyNow.tr(), + onPressed: state.canSubmit + ? _completeOrder + : null, + ), + ), + const SizedBox(height: 16), + Text( + _isLoggedIn + ? state.fiatOrderStatus.isFailed + ? LocaleKeys.fiatCantCompleteOrder.tr() + : LocaleKeys.fiatPriceCanChange.tr() + : LocaleKeys.fiatConnectWallet.tr(), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), ), ), - ), - ], + ], + ), ), ), ), @@ -157,8 +179,11 @@ class _FiatFormState extends State { ); } - void _completeOrder() => - context.read().add(const FiatFormSubmitted()); + void _completeOrder() { + // Dismiss keyboard before submitting + FocusScope.of(context).unfocus(); + context.read().add(const FiatFormSubmitted()); + } void _onFiatChanged(FiatCurrency value) => context.read() ..add(FiatFormFiatSelected(value)) @@ -208,9 +233,9 @@ class _FiatFormState extends State { } void _onConsoleMessage(String message) { - context - .read() - .add(FiatFormPaymentStatusMessageReceived(message)); + context.read().add( + FiatFormPaymentStatusMessageReceived(message), + ); } void _onCloseWebView() { @@ -277,7 +302,8 @@ class _FiatFormState extends State { title = LocaleKeys.fiatPaymentFailedTitle.tr(); content = LocaleKeys.fiatPaymentFailedMessage.tr(); if (state.providerError != null && state.providerError!.isNotEmpty) { - content = '$content\n\n${LocaleKeys.errorDetails.tr()}: ' + content = + '$content\n\n${LocaleKeys.errorDetails.tr()}: ' '${state.providerError}'; } icon = const Icon(Icons.error_outline, color: Colors.red); @@ -306,15 +332,13 @@ extension on FiatAmountValidationError { final fiatId = state.selectedFiat.value?.symbol ?? ''; switch (this) { case FiatAmountValidationError.aboveMaximum: - return LocaleKeys.fiatMaximumAmount - .tr(args: [state.maxFiatAmount?.toString() ?? '', fiatId]); + return LocaleKeys.fiatMaximumAmount.tr( + args: [state.maxFiatAmount?.toString() ?? '', fiatId], + ); case FiatAmountValidationError.invalid: case FiatAmountValidationError.belowMinimum: return LocaleKeys.fiatMinimumAmount.tr( - args: [ - state.minFiatAmount?.toStringAsFixed(2) ?? '', - fiatId, - ], + args: [state.minFiatAmount?.toStringAsFixed(2) ?? '', fiatId], ); case FiatAmountValidationError.empty: return null; diff --git a/lib/views/fiat/fiat_inputs.dart b/lib/views/fiat/fiat_inputs.dart index 8ed83e5f4b..b3a8427cfa 100644 --- a/lib/views/fiat/fiat_inputs.dart +++ b/lib/views/fiat/fiat_inputs.dart @@ -60,12 +60,14 @@ class FiatInputs extends StatefulWidget { class FiatInputsState extends State { TextEditingController fiatController = TextEditingController(); late final Debouncer _debouncer; + late final FocusNode _fiatFocusNode; bool _hasUserInput = false; @override void dispose() { fiatController.dispose(); _debouncer.dispose(); + _fiatFocusNode.dispose(); super.dispose(); } @@ -73,6 +75,7 @@ class FiatInputsState extends State { void initState() { super.initState(); _debouncer = Debouncer(duration: const Duration(milliseconds: 300)); + _fiatFocusNode = FocusNode(); fiatController.text = widget.initialFiatAmount?.toString() ?? ''; } @@ -128,6 +131,13 @@ class FiatInputsState extends State { }); } + void _onFiatAmountSubmitted(String value) { + // Dismiss keyboard when user presses done/enter + _fiatFocusNode.unfocus(); + // Trigger final update if needed + widget.onFiatAmountUpdate(value); + } + @override Widget build(BuildContext context) { // TODO: refactor currency type to use AssetId/Asset to avoid @@ -153,8 +163,10 @@ class FiatInputsState extends State { CustomFiatInputField( key: const Key('fiat-amount-form-field'), controller: fiatController, + focusNode: _fiatFocusNode, hintText: '${LocaleKeys.enterAmount.tr()} $boundariesString', onTextChanged: fiatAmountChanged, + onSubmitted: _onFiatAmountSubmitted, label: Text(LocaleKeys.spend.tr()), assetButton: FiatCurrencyItem( key: const Key('fiat-onramp-fiat-dropdown'), @@ -204,18 +216,20 @@ class FiatInputsState extends State { ), ), const SizedBox(width: 12), - SizedBox( - height: 48, - child: FiatCurrencyItem( - key: const Key('fiat-onramp-coin-dropdown'), - foregroundColor: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - disabled: coinListLoading, - currency: widget.selectedAsset, - icon: Icon(_getDefaultAssetIcon('coin')), - onTap: () => _showAssetSelectionDialog('coin'), - isListTile: false, + Flexible( + child: SizedBox( + height: 48, + child: FiatCurrencyItem( + key: const Key('fiat-onramp-coin-dropdown'), + foregroundColor: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + disabled: coinListLoading, + currency: widget.selectedAsset, + icon: Icon(_getDefaultAssetIcon('coin')), + onTap: () => _showAssetSelectionDialog('coin'), + isListTile: false, + ), ), ), ], @@ -245,6 +259,9 @@ class FiatInputsState extends State { } void _showAssetSelectionDialog(String type) { + // Dismiss keyboard before showing dialog + _fiatFocusNode.unfocus(); + final isFiat = type == 'fiat'; final icon = Icon(_getDefaultAssetIcon(type)); diff --git a/lib/views/fiat/fiat_select_button.dart b/lib/views/fiat/fiat_select_button.dart index fd44a9a517..60c8f10ee2 100644 --- a/lib/views/fiat/fiat_select_button.dart +++ b/lib/views/fiat/fiat_select_button.dart @@ -36,36 +36,38 @@ class FiatSelectButton extends StatelessWidget { onPressed: enabled ? onTap : null, label: Row( mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - (isFiat ? currency?.getAbbr() : currency?.name) ?? - (isFiat - ? LocaleKeys.selectFiat.tr() - : LocaleKeys.selectCoin.tr()), - style: DefaultTextStyle.of(context).style.copyWith( - fontWeight: FontWeight.w500, - color: enabled - ? foregroundColor - : foregroundColor.withValues(alpha: 0.5), - ), - ), - if (!isFiat && cryptoCurrency != null) + if (isFiat) + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ Text( - getCoinTypeName( - cryptoCurrency.chainType, - cryptoCurrency.symbol, - ), + (isFiat ? currency?.getAbbr() : currency?.name) ?? + (isFiat + ? LocaleKeys.selectFiat.tr() + : LocaleKeys.selectCoin.tr()), style: DefaultTextStyle.of(context).style.copyWith( + fontWeight: FontWeight.w500, color: enabled - ? foregroundColor.withValues(alpha: 0.5) - : foregroundColor.withValues(alpha: 0.25), + ? foregroundColor + : foregroundColor.withValues(alpha: 0.5), ), ), - ], - ), + if (!isFiat && cryptoCurrency != null) + Text( + getCoinTypeName( + cryptoCurrency.chainType, + cryptoCurrency.symbol, + ), + style: DefaultTextStyle.of(context).style.copyWith( + color: enabled + ? foregroundColor.withValues(alpha: 0.5) + : foregroundColor.withValues(alpha: 0.25), + ), + ), + ], + ), const SizedBox(width: 4), Icon( Icons.keyboard_arrow_down, From 51e04aa36f126e63e4e40faf83699e2967807b13 Mon Sep 17 00:00:00 2001 From: Francois Date: Fri, 24 Oct 2025 04:21:26 +0200 Subject: [PATCH 3/9] fix(coins-list): alignment and spacing of text in the active coins list coins with long names, like BAT, would overflow and cause alignment issues. Fixed with expanded and flex --- .../common/expandable_coin_list_item.dart | 125 +++++++++--------- 1 file changed, 66 insertions(+), 59 deletions(-) diff --git a/lib/views/wallet/wallet_page/common/expandable_coin_list_item.dart b/lib/views/wallet/wallet_page/common/expandable_coin_list_item.dart index b0ac070a09..8f07f9fed2 100644 --- a/lib/views/wallet/wallet_page/common/expandable_coin_list_item.dart +++ b/lib/views/wallet/wallet_page/common/expandable_coin_list_item.dart @@ -147,69 +147,76 @@ class _ExpandableCoinListItemState extends State { // Use CoinItem with large size for mobile, matching GroupedAssetTickerItem AssetIcon(widget.coin.id, size: CoinItemSize.large.coinLogo), const SizedBox(width: 8), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Coin name - using headlineMedium for bold 16px text - Text( - widget.coin.displayName, - style: theme.textTheme.headlineMedium, - ), - // Crypto balance - using bodySmall for 12px secondary text - Text( - '${doubleToString(widget.coin.balance(context.sdk) ?? 0)} ${widget.coin.abbr}', - style: theme.textTheme.bodySmall, - ), - ], + Expanded( + flex: 8, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Coin name - using headlineMedium for bold 16px text + AutoScrollText( + text: widget.coin.displayName, + style: theme.textTheme.headlineMedium, + ), + // Crypto balance - using bodySmall for 12px secondary text + AutoScrollText( + text: + '${doubleToString(widget.coin.balance(context.sdk) ?? 0)} ${widget.coin.abbr}', + style: theme.textTheme.bodySmall, + ), + ], + ), ), const Spacer(), // Right side: Price and trend info - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - // Current balance in USD - using headlineMedium for bold 16px text - if (widget.coin.lastKnownUsdBalance(context.sdk) != null) - Text( - '\$${NumberFormat("#,##0.00").format(widget.coin.lastKnownUsdBalance(context.sdk)!)}', - style: theme.textTheme.headlineMedium, - ), - const SizedBox(height: 2), - // Trend percentage - BlocBuilder( - builder: (context, state) { - final usdBalance = widget.coin.lastKnownUsdBalance( - context.sdk, - ); - if (usdBalance == null) { - return const SizedBox.shrink(); - } + Expanded( + flex: 7, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // Current balance in USD - using headlineMedium for bold 16px text + if (widget.coin.lastKnownUsdBalance(context.sdk) != null) + Text( + '\$${NumberFormat("#,##0.00").format(widget.coin.lastKnownUsdBalance(context.sdk)!)}', + style: theme.textTheme.headlineMedium, + ), + const SizedBox(height: 2), + // Trend percentage + BlocBuilder( + builder: (context, state) { + final usdBalance = widget.coin.lastKnownUsdBalance( + context.sdk, + ); + if (usdBalance == null) { + return const SizedBox.shrink(); + } - final change24hPercent = usdBalance == 0.0 - ? 0.0 - : state.get24hChangeForAsset(widget.coin.id); - // Calculate the 24h USD change value - final change24hValue = - change24hPercent != null && usdBalance > 0 - ? (change24hPercent * usdBalance / 100) - : 0.0; - final themeCustom = - Theme.of(context).brightness == Brightness.dark - ? Theme.of(context).extension()! - : Theme.of(context).extension()!; - return TrendPercentageText( - percentage: change24hPercent, - value: change24hValue, - upColor: themeCustom.increaseColor, - downColor: themeCustom.decreaseColor, - valueFormatter: (value) => - NumberFormat.currency(symbol: '\$').format(value), - iconSize: 12, - spacing: 2, - textStyle: theme.textTheme.bodySmall, - ); - }, - ), - ], + final change24hPercent = usdBalance == 0.0 + ? 0.0 + : state.get24hChangeForAsset(widget.coin.id); + // Calculate the 24h USD change value + final change24hValue = + change24hPercent != null && usdBalance > 0 + ? (change24hPercent * usdBalance / 100) + : 0.0; + final themeCustom = + Theme.of(context).brightness == Brightness.dark + ? Theme.of(context).extension()! + : Theme.of(context).extension()!; + return TrendPercentageText( + percentage: change24hPercent, + value: change24hValue, + upColor: themeCustom.increaseColor, + downColor: themeCustom.decreaseColor, + valueFormatter: (value) => + NumberFormat.currency(symbol: '\$').format(value), + iconSize: 12, + spacing: 2, + textStyle: theme.textTheme.bodySmall, + ); + }, + ), + ], + ), ), ], ), From af47946f062f98f3249286d6c056b868dfd8fea5 Mon Sep 17 00:00:00 2001 From: Francois Date: Fri, 24 Oct 2025 07:13:07 +0200 Subject: [PATCH 4/9] fix(fiat-onramp): text alignment and scaling for longer names --- lib/views/fiat/fiat_asset_icon.dart | 126 ++++++++++++++++++++++++- lib/views/fiat/fiat_select_button.dart | 13 ++- 2 files changed, 132 insertions(+), 7 deletions(-) diff --git a/lib/views/fiat/fiat_asset_icon.dart b/lib/views/fiat/fiat_asset_icon.dart index 216ef515f0..a77f2014ee 100644 --- a/lib/views/fiat/fiat_asset_icon.dart +++ b/lib/views/fiat/fiat_asset_icon.dart @@ -1,10 +1,16 @@ +import 'package:app_theme/app_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_ui/komodo_ui.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/coins_bloc/asset_coin_extension.dart'; import 'package:web_dex/bloc/fiat/models/i_currency.dart'; -import 'package:web_dex/shared/widgets/coin_item/coin_item.dart' show CoinItem; +import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/widgets/coin_item/coin_item_size.dart'; +import 'package:web_dex/shared/widgets/coin_item/coin_protocol_name.dart'; +import 'package:web_dex/shared/widgets/coin_item/coin_ticker.dart'; +import 'package:web_dex/shared/widgets/segwit_icon.dart'; import 'package:web_dex/views/fiat/fiat_icon.dart'; class FiatAssetIcon extends StatelessWidget { @@ -14,12 +20,14 @@ class FiatAssetIcon extends StatelessWidget { required this.onTap, required this.assetExists, super.key, + this.expanded = false, }); final ICurrency currency; final Widget icon; final VoidCallback onTap; final bool? assetExists; + final bool expanded; @override Widget build(BuildContext context) { @@ -29,6 +37,120 @@ class FiatAssetIcon extends StatelessWidget { final sdk = RepositoryProvider.of(context); final asset = sdk.getSdkAsset(currency.getAbbr()); - return CoinItem(coin: asset.toCoin(), size: CoinItemSize.large); + final coin = asset.toCoin(); + final size = CoinItemSize.large; + + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AssetLogo.ofId(coin.id, size: size.coinLogo), + SizedBox(width: size.spacer), + expanded + ? Expanded( + child: _FiatCoinItemLabel(size: size, coin: coin), + ) + : Flexible( + child: _FiatCoinItemLabel(size: size, coin: coin), + ), + ], + ); + } +} + +class _FiatCoinItemLabel extends StatelessWidget { + const _FiatCoinItemLabel({super.key, required this.size, required this.coin}); + + final CoinItemSize size; + final Coin coin; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: size.spacer), + _FiatCoinItemTitle(coin: coin, size: size), + SizedBox(height: size.spacer), + _FiatCoinItemSubtitle(coin: coin, size: size), + ], + ); + } +} + +class _FiatCoinItemTitle extends StatelessWidget { + const _FiatCoinItemTitle({required this.coin, required this.size}); + + final Coin? coin; + final CoinItemSize size; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final showCoinName = constraints.maxWidth > 75; + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + CoinTicker( + coinId: coin?.abbr, + style: TextStyle(fontSize: size.titleFontSize, height: 1), + showSuffix: false, + ), + if (showCoinName) ...[ + SizedBox(width: size.spacer), + Flexible( + child: _FiatCoinName( + text: coin?.displayName, + style: TextStyle(fontSize: size.titleFontSize, height: 1), + ), + ), + ], + ], + ); + }, + ); + } +} + +class _FiatCoinItemSubtitle extends StatelessWidget { + const _FiatCoinItemSubtitle({required this.coin, required this.size}); + + final Coin? coin; + final CoinItemSize size; + + @override + Widget build(BuildContext context) { + return coin?.mode == CoinMode.segwit + ? SegwitIcon(height: size.segwitIconSize) + : CoinProtocolName( + text: coin?.typeNameWithTestnet, + upperCase: true, + size: size, + ); + } +} + +class _FiatCoinName extends StatelessWidget { + const _FiatCoinName({required this.text, this.style}); + + final String? text; + final TextStyle? style; + + @override + Widget build(BuildContext context) { + final String? coinName = text; + if (coinName == null) return const SizedBox.shrink(); + + return AutoScrollText( + text: coinName, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: theme.custom.dexCoinProtocolColor, + ).merge(style), + ); } } diff --git a/lib/views/fiat/fiat_select_button.dart b/lib/views/fiat/fiat_select_button.dart index 60c8f10ee2..04dc739d97 100644 --- a/lib/views/fiat/fiat_select_button.dart +++ b/lib/views/fiat/fiat_select_button.dart @@ -90,11 +90,14 @@ class FiatSelectButton extends StatelessWidget { ), icon: currency == null ? Icon(_getDefaultAssetIcon(isFiat ? 'fiat' : 'coin')) - : FiatAssetIcon( - currency: currency!, - icon: icon, - onTap: onTap, - assetExists: assetExists, + : Flexible( + child: FiatAssetIcon( + currency: currency!, + icon: icon, + onTap: onTap, + assetExists: assetExists, + expanded: true, + ), ), ); } From bf8774ce9dd7677f6aecd62cba87913bf2329731 Mon Sep 17 00:00:00 2001 From: Francois Date: Fri, 24 Oct 2025 07:33:36 +0200 Subject: [PATCH 5/9] fix(fiat-onramp): add komodo and sandbox domains to whitelist --- .../fiat/fiat_onramp_form/fiat_form_bloc.dart | 5 +++-- .../fiat/fiat_onramp_form/fiat_form_state.dart | 3 +++ lib/bloc/fiat/fiat_order_status.dart | 5 +++++ lib/views/fiat/fiat_form.dart | 1 + .../fiat/fiat_provider_web_view_settings.dart | 16 ++++++---------- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart b/lib/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart index 979b0012a9..08c7a47be9 100644 --- a/lib/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart +++ b/lib/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart @@ -169,6 +169,8 @@ class FiatFormBloc extends Bloc { FiatFormSubmitted event, Emitter emit, ) async { + emit(state.copyWith(fiatOrderStatus: FiatOrderStatus.submitting)); + final formValidationError = getFormIssue(); if (formValidationError != null || !state.isValid) { _log.warning('Form validation failed. Validation: ${state.isValid}'); @@ -229,12 +231,11 @@ class FiatFormBloc extends Bloc { WebViewDialogMode _determineWebViewMode() { final bool isLinux = !kIsWeb && !kIsWasm && Platform.isLinux; const bool isWeb = kIsWeb || kIsWasm; - final bool isBanxa = state.selectedPaymentMethod.providerId == 'Banxa'; // Banxa "Return to Komodo" button attempts to navigate the top window to // the return URL, which is not supported in a dialog. So we need to open // it in a new tab. - if (isLinux || (isWeb && isBanxa)) { + if (isLinux || (isWeb && state.isBanxaSelected)) { return WebViewDialogMode.newTab; } else if (isWeb) { return WebViewDialogMode.dialog; diff --git a/lib/bloc/fiat/fiat_onramp_form/fiat_form_state.dart b/lib/bloc/fiat/fiat_onramp_form/fiat_form_state.dart index 2c266731e1..bb1bc54593 100644 --- a/lib/bloc/fiat/fiat_onramp_form/fiat_form_state.dart +++ b/lib/bloc/fiat/fiat_onramp_form/fiat_form_state.dart @@ -120,6 +120,9 @@ final class FiatFormState extends Equatable with FormzMixin { !fiatOrderStatus.isSubmitting && isValid; + /// Whether the selected payment method is from the Banxa provider + bool get isBanxaSelected => selectedPaymentMethod.providerId == 'Banxa'; + FiatFormState copyWith({ CurrencyInput? selectedFiat, CurrencyInput? selectedAsset, diff --git a/lib/bloc/fiat/fiat_order_status.dart b/lib/bloc/fiat/fiat_order_status.dart index 72bcf77a31..d76ae4682d 100644 --- a/lib/bloc/fiat/fiat_order_status.dart +++ b/lib/bloc/fiat/fiat_order_status.dart @@ -4,6 +4,10 @@ import 'package:logging/logging.dart'; enum FiatOrderStatus { /// Initial status: User has not yet started the payment process initial, + + /// User has opened the webview to go to the provider to proceed with the + /// purchase + submitting, /// User has started the process, and the payment method has been opened. /// E.g. Ramp or Banxa websites have been opened @@ -30,6 +34,7 @@ enum FiatOrderStatus { bool get isSubmitting => this == FiatOrderStatus.inProgress || this == FiatOrderStatus.submitted || + this == FiatOrderStatus.submitting || this == FiatOrderStatus.pendingPayment; bool get isFailed => this == FiatOrderStatus.failed; bool get isSuccess => this == FiatOrderStatus.success; diff --git a/lib/views/fiat/fiat_form.dart b/lib/views/fiat/fiat_form.dart index af16121690..9fae151761 100644 --- a/lib/views/fiat/fiat_form.dart +++ b/lib/views/fiat/fiat_form.dart @@ -287,6 +287,7 @@ class _FiatFormState extends State { case FiatOrderStatus.inProgress: case FiatOrderStatus.windowCloseRequested: case FiatOrderStatus.initial: + case FiatOrderStatus.submitting: case FiatOrderStatus.pendingPayment: debugPrint('Pending status should not be shown in dialog.'); return; diff --git a/lib/views/fiat/fiat_provider_web_view_settings.dart b/lib/views/fiat/fiat_provider_web_view_settings.dart index ea62c52b23..c0d8ee99f0 100644 --- a/lib/views/fiat/fiat_provider_web_view_settings.dart +++ b/lib/views/fiat/fiat_provider_web_view_settings.dart @@ -4,9 +4,11 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart'; /// Default trusted domains for the WebView content blockers const List kDefaultTrustedDomainFilters = [ r'komodo\.banxa\.com.*', - r'app\.demo\.ramp\.network.*', + if (kDebugMode) r'app\.demo\.ramp\.network.*', r'app\.ramp\.network.*', r'embed\.bitrefill\.com.*', + if (kDebugMode) r'komodo\.banxa-sandbox\.com.*', + r'app\.komodoplatform\.com.*', ]; /// Factory methods for creating webview settings for specific providers @@ -39,19 +41,13 @@ class FiatProviderWebViewSettings { contentBlockers: [ // Block all content by default ContentBlocker( - trigger: ContentBlockerTrigger( - urlFilter: '.*', - ), - action: ContentBlockerAction( - type: ContentBlockerActionType.BLOCK, - ), + trigger: ContentBlockerTrigger(urlFilter: '.*'), + action: ContentBlockerAction(type: ContentBlockerActionType.BLOCK), ), // Allow the specific domains we trust ...trustedDomainFilters.map( (urlFilter) => ContentBlocker( - trigger: ContentBlockerTrigger( - urlFilter: urlFilter, - ), + trigger: ContentBlockerTrigger(urlFilter: urlFilter), action: ContentBlockerAction( type: ContentBlockerActionType.IGNORE_PREVIOUS_RULES, ), From a2237c186e94932a052c44c4b5774e49f5803bf4 Mon Sep 17 00:00:00 2001 From: Francois Date: Fri, 24 Oct 2025 10:24:54 +0200 Subject: [PATCH 6/9] fix(fiat-onramp): disable overly restrictive CSP with limited platform support --- .../fiat/fiat_provider_web_view_settings.dart | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/lib/views/fiat/fiat_provider_web_view_settings.dart b/lib/views/fiat/fiat_provider_web_view_settings.dart index c0d8ee99f0..5ba8e1c1c9 100644 --- a/lib/views/fiat/fiat_provider_web_view_settings.dart +++ b/lib/views/fiat/fiat_provider_web_view_settings.dart @@ -38,22 +38,26 @@ class FiatProviderWebViewSettings { // Deliberately NOT including ALLOW_TOP_NAVIGATION to prevent // parent navigation }, - contentBlockers: [ - // Block all content by default - ContentBlocker( - trigger: ContentBlockerTrigger(urlFilter: '.*'), - action: ContentBlockerAction(type: ContentBlockerActionType.BLOCK), - ), - // Allow the specific domains we trust - ...trustedDomainFilters.map( - (urlFilter) => ContentBlocker( - trigger: ContentBlockerTrigger(urlFilter: urlFilter), - action: ContentBlockerAction( - type: ContentBlockerActionType.IGNORE_PREVIOUS_RULES, - ), - ), - ), - ], + // TODO: revisit & possibly fork repo to add support for more platforms + // The intended approach only works on iOS and macOS, and is far too restrictive + // given that we are using 3rd party providers that can change their dependencies at + // any time (i.e. Ramp)tk + // contentBlockers: [ + // // Block all content by default + // ContentBlocker( + // trigger: ContentBlockerTrigger(urlFilter: '.*'), + // action: ContentBlockerAction(type: ContentBlockerActionType.BLOCK), + // ), + // // Allow the specific domains we trust + // ...trustedDomainFilters.map( + // (urlFilter) => ContentBlocker( + // trigger: ContentBlockerTrigger(urlFilter: urlFilter), + // action: ContentBlockerAction( + // type: ContentBlockerActionType.IGNORE_PREVIOUS_RULES, + // ), + // ), + // ), + // ], ); } } From 55174f35732cfd39917ef681a7be20b092e5e36d Mon Sep 17 00:00:00 2001 From: Francois Date: Fri, 24 Oct 2025 10:59:11 +0200 Subject: [PATCH 7/9] refactor: remove unused widget and update enum docs --- lib/bloc/fiat/fiat_order_status.dart | 18 +++++++++++------- lib/bloc/taker_form/taker_bloc.dart | 5 +++++ lib/views/fiat/fiat_asset_icon.dart | 3 ++- lib/views/fiat/fiat_select_button.dart | 12 ------------ 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/lib/bloc/fiat/fiat_order_status.dart b/lib/bloc/fiat/fiat_order_status.dart index d76ae4682d..9af4b2a911 100644 --- a/lib/bloc/fiat/fiat_order_status.dart +++ b/lib/bloc/fiat/fiat_order_status.dart @@ -4,13 +4,16 @@ import 'package:logging/logging.dart'; enum FiatOrderStatus { /// Initial status: User has not yet started the payment process initial, - - /// User has opened the webview to go to the provider to proceed with the - /// purchase + + /// Form is being submitted: Order is being created with the provider. + /// This is the transitional state while waiting for the provider API + /// response with the checkout URL. submitting, - /// User has started the process, and the payment method has been opened. - /// E.g. Ramp or Banxa websites have been opened + /// Order successfully created and checkout URL received from provider. + /// The webview dialog is ready to be opened/is opening with the provider's + /// payment page (e.g., Ramp or Banxa). This state triggers the webview + /// display and starts the order status monitoring. submitted, /// Payment is awaiting user action (e.g., user needs to complete payment) @@ -71,8 +74,9 @@ enum FiatOrderStatus { // to avoid alarming users with "Payment failed" popup messages // unless we are sure that the payment has failed. // Ideally, this section should not be reached. - Logger('FiatOrderStatus') - .warning('Unknown status: $status, defaulting to in progress'); + Logger( + 'FiatOrderStatus', + ).warning('Unknown status: $status, defaulting to in progress'); return FiatOrderStatus.inProgress; } } diff --git a/lib/bloc/taker_form/taker_bloc.dart b/lib/bloc/taker_form/taker_bloc.dart index 90b9a57803..4c603c753b 100644 --- a/lib/bloc/taker_form/taker_bloc.dart +++ b/lib/bloc/taker_form/taker_bloc.dart @@ -499,6 +499,11 @@ class TakerBloc extends Bloc { } } catch (e, s) { _log.severe('Failed to update max sell amount', e, s); + emitter( + state.copyWith( + availableBalanceState: () => AvailableBalanceState.failure, + ), + ); } } diff --git a/lib/views/fiat/fiat_asset_icon.dart b/lib/views/fiat/fiat_asset_icon.dart index a77f2014ee..a1a0ec7eab 100644 --- a/lib/views/fiat/fiat_asset_icon.dart +++ b/lib/views/fiat/fiat_asset_icon.dart @@ -41,7 +41,7 @@ class FiatAssetIcon extends StatelessWidget { final size = CoinItemSize.large; return Row( - mainAxisSize: MainAxisSize.min, + mainAxisSize: expanded ? MainAxisSize.max : MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ AssetLogo.ofId(coin.id, size: size.coinLogo), @@ -51,6 +51,7 @@ class FiatAssetIcon extends StatelessWidget { child: _FiatCoinItemLabel(size: size, coin: coin), ) : Flexible( + fit: expanded ? FlexFit.tight : FlexFit.loose, child: _FiatCoinItemLabel(size: size, coin: coin), ), ], diff --git a/lib/views/fiat/fiat_select_button.dart b/lib/views/fiat/fiat_select_button.dart index 04dc739d97..e27f2141bb 100644 --- a/lib/views/fiat/fiat_select_button.dart +++ b/lib/views/fiat/fiat_select_button.dart @@ -54,18 +54,6 @@ class FiatSelectButton extends StatelessWidget { : foregroundColor.withValues(alpha: 0.5), ), ), - if (!isFiat && cryptoCurrency != null) - Text( - getCoinTypeName( - cryptoCurrency.chainType, - cryptoCurrency.symbol, - ), - style: DefaultTextStyle.of(context).style.copyWith( - color: enabled - ? foregroundColor.withValues(alpha: 0.5) - : foregroundColor.withValues(alpha: 0.25), - ), - ), ], ), const SizedBox(width: 4), From 6e867dab06f7db7b19a5cf150a0c348e7141a91e Mon Sep 17 00:00:00 2001 From: Francois Date: Fri, 24 Oct 2025 21:18:41 +0200 Subject: [PATCH 8/9] refactor(fiat-onramp): deprecate legacy validation and move emit down --- .../fiat/fiat_onramp_form/fiat_form_bloc.dart | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/lib/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart b/lib/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart index 08c7a47be9..29ec7a74a0 100644 --- a/lib/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart +++ b/lib/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart @@ -44,7 +44,8 @@ class FiatFormBloc extends Bloc { // can happen frequently and we want to avoid race conditions. on(_onCoinSelected, transformer: restartable()); on(_onPaymentMethodSelected); - on(_onFormSubmitted); + // Use droppable here to prevent multiple simultaneous submissions + on(_onFormSubmitted, transformer: droppable()); on(_onPaymentStatusMessage); on(_onModeUpdated); on(_onAccountCleared); @@ -169,17 +170,18 @@ class FiatFormBloc extends Bloc { FiatFormSubmitted event, Emitter emit, ) async { - emit(state.copyWith(fiatOrderStatus: FiatOrderStatus.submitting)); - - final formValidationError = getFormIssue(); + final formValidationError = _getFormIssue(); if (formValidationError != null || !state.isValid) { _log.warning('Form validation failed. Validation: ${state.isValid}'); return; } - if (state.checkoutUrl.isNotEmpty) { - emit(state.copyWith(checkoutUrl: '')); - } + emit( + state.copyWith( + fiatOrderStatus: FiatOrderStatus.submitting, + checkoutUrl: '', + ), + ); try { final newOrder = await _fiatRepository.buyCoin( @@ -416,7 +418,12 @@ class FiatFormBloc extends Bloc { ); } - String? getFormIssue() { + @Deprecated( + 'Validation is handled by formz in dedicated inputs like [FiatAmountInput]' + 'This function should be removed once the cases are confirmed to be ' + 'covered by formz inputs', + ) + String? _getFormIssue() { // TODO: ? show on the UI and localise? These are currently used as more of // a boolean "is there an error?" rather than "what is the error?" if (state.paymentMethods.isEmpty) { From 4ff4a6e112bfa5043a4a41579868af12c4abe9d8 Mon Sep 17 00:00:00 2001 From: Francois Date: Tue, 28 Oct 2025 20:54:56 +0200 Subject: [PATCH 9/9] chore(fiat-onramp): add banxa iframe allow list --- assets/web_pages/fiat_widget.html | 22 ++++++++-------- .../fiat/fiat_provider_web_view_settings.dart | 25 +++++-------------- 2 files changed, 17 insertions(+), 30 deletions(-) diff --git a/assets/web_pages/fiat_widget.html b/assets/web_pages/fiat_widget.html index 67427c2307..88d3651a8b 100644 --- a/assets/web_pages/fiat_widget.html +++ b/assets/web_pages/fiat_widget.html @@ -28,7 +28,7 @@