From afbc6db06621dd8a0d67153f2d44bff4d7f628b5 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Wed, 1 Jul 2026 16:49:19 +0200 Subject: [PATCH] wip --- lib/di.dart | 5 +- .../pages/buy_sell/buy_sell_amount_page.dart | 110 +++++++++++++++--- .../buy_sell/buy_sell_provider_page.dart | 45 +++++++ lib/new-ui/widgets/floating_amount_input.dart | 92 +++++++++++++++ lib/src/screens/buy/buy_sell_page.dart | 45 +------ lib/view_model/buy/buy_sell_view_model.dart | 33 +++--- 6 files changed, 255 insertions(+), 75 deletions(-) create mode 100644 lib/new-ui/pages/buy_sell/buy_sell_provider_page.dart create mode 100644 lib/new-ui/widgets/floating_amount_input.dart diff --git a/lib/di.dart b/lib/di.dart index a4ae25ca9..faf6196af 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1494,10 +1494,11 @@ Future setup({ getIt.registerFactory(() => BuyAmountViewModel()); - getIt.registerFactory(() => BuySellViewModel(getIt.get())); + getIt.registerFactoryParam( + (mode, _) => BuySellViewModel(mode: mode, getIt.get())); getIt.registerFactoryParam((mode, _) => - NewBuySellAmountPage(mode: mode, buySellViewModel: getIt.get())); + NewBuySellAmountPage(buySellViewModel: getIt.get(param1: mode))); getIt.registerFactory(() => BuySellPage(getIt.get())); diff --git a/lib/new-ui/pages/buy_sell/buy_sell_amount_page.dart b/lib/new-ui/pages/buy_sell/buy_sell_amount_page.dart index 3b36c5834..327008938 100644 --- a/lib/new-ui/pages/buy_sell/buy_sell_amount_page.dart +++ b/lib/new-ui/pages/buy_sell/buy_sell_amount_page.dart @@ -1,16 +1,19 @@ import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart'; +import 'package:cake_wallet/new-ui/widgets/floating_amount_input.dart'; +import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; import 'package:cake_wallet/themes/core/theme_extension.dart'; import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart'; import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; class NewBuySellAmountPage extends StatefulWidget { - const NewBuySellAmountPage({super.key, required this.mode, required this.buySellViewModel}); + const NewBuySellAmountPage({super.key, required this.buySellViewModel}); - final BuySellPageMode mode; final BuySellViewModel buySellViewModel; @override @@ -19,6 +22,7 @@ class NewBuySellAmountPage extends StatefulWidget { class _NewBuySellAmountPageState extends State { bool _customAmountMode = false; + final customInputController = TextEditingController(); @override Widget build(BuildContext context) { @@ -40,22 +44,37 @@ class _NewBuySellAmountPageState extends State { ModalTopBar( title: _pageTitle, leadingIcon: Icon(Icons.close), - onLeadingPressed: Navigator.of(context).pop, + onLeadingPressed: Navigator.of(context, rootNavigator: true).pop, ), Expanded( - child: BuySellDefaultAmountSelector( - defaultAmounts: widget.buySellViewModel.defaultAmounts, - currency: widget.buySellViewModel.fiatCurrency, - mode: widget.mode, - onSelected: (amount) { - if (amount == null) { - setState(() { - _customAmountMode = true; - }); - } else { - widget.buySellViewModel.changeFiatAmount(amount: amount); - } - }, + child: AnimatedSwitcher( + duration: Duration(milliseconds: 300), + child: _customAmountMode + ? Observer( + builder: (_) => BuySellCustomAmountInput( + fiatCurrency: widget.buySellViewModel.fiatCurrency, + cryptoCurrency: widget.buySellViewModel.cryptoCurrency, + cryptoAmount: widget.buySellViewModel.cryptoAmount, + controller: customInputController, + onContinuePressed: () {}, + onChanged: (amount) => + widget.buySellViewModel.changeFiatAmount(amount: amount), + )) + : BuySellDefaultAmountSelector( + key: ValueKey(0), + defaultAmounts: widget.buySellViewModel.defaultAmounts, + currency: widget.buySellViewModel.fiatCurrency, + mode: widget.buySellViewModel.mode, + onSelected: (amount) { + if (amount == null) { + setState(() { + _customAmountMode = true; + }); + } else { + widget.buySellViewModel.changeFiatAmount(amount: amount); + } + }, + ), )) ], ), @@ -63,7 +82,7 @@ class _NewBuySellAmountPageState extends State { ); } - String get _pageTitle => widget.mode == BuySellPageMode.buy + String get _pageTitle => widget.buySellViewModel.mode == BuySellPageMode.buy ? S.current.buy : S.current.sell + ((widget.buySellViewModel.cryptoCurrencies.length == 1) @@ -71,6 +90,62 @@ class _NewBuySellAmountPageState extends State { : ""); } +class BuySellCustomAmountInput extends StatelessWidget { + const BuySellCustomAmountInput( + {super.key, + required this.fiatCurrency, + required this.cryptoCurrency, + required this.cryptoAmount, + required this.controller, + required this.onContinuePressed, + required this.onChanged}); + + final FiatCurrency fiatCurrency; + final CryptoCurrency cryptoCurrency; + final String cryptoAmount; + final TextEditingController controller; + final VoidCallback onContinuePressed; + final Function(String) onChanged; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox.shrink(), + Column( + spacing: 8, + children: [ + FloatingAmountInput( + currency: fiatCurrency, + controller: controller, + onChanged: onChanged, + ), + Opacity( + opacity: cryptoAmount.isEmpty ? 0 : 1, + child: Text( + "≈ ${cryptoAmount} ${cryptoCurrency.symbol}", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + ) + ], + ), + Padding( + padding: const EdgeInsets.all(18.0), + child: NewPrimaryButton( + onPressed: onContinuePressed, + text: S.of(context).continue_text, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary), + ) + ], + ); + } +} + class BuySellDefaultAmountSelector extends StatelessWidget { const BuySellDefaultAmountSelector( {super.key, @@ -100,6 +175,7 @@ class BuySellDefaultAmountSelector extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 18.0), child: GridView.builder( shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), // +1 for "custom" option itemCount: defaultAmounts.length + 1, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( diff --git a/lib/new-ui/pages/buy_sell/buy_sell_provider_page.dart b/lib/new-ui/pages/buy_sell/buy_sell_provider_page.dart new file mode 100644 index 000000000..acbc7f71e --- /dev/null +++ b/lib/new-ui/pages/buy_sell/buy_sell_provider_page.dart @@ -0,0 +1,45 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart'; +import 'package:flutter/material.dart'; + +class BuySellProviderPage extends StatelessWidget { + const BuySellProviderPage({super.key, required this.buySellViewModel}); + + final BuySellViewModel buySellViewModel; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical(top: Radius.circular(18)), + gradient: LinearGradient( + colors: [ + Theme.of(context).colorScheme.surface, + Theme.of(context).colorScheme.surfaceDim, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: SafeArea( + child: Column( + children: [ + ModalTopBar( + title: _pageTitle, + leadingIcon: Icon(Icons.arrow_back_ios_new), + onLeadingPressed: Navigator.of(context).pop, + ), + Expanded(child: Column( + + )) + ], + )), + ); + } + + String get _pageTitle => buySellViewModel.mode == BuySellPageMode.buy + ? S.current.buy + : S.current.sell + " " + (buySellViewModel.cryptoCurrency.fullName ?? ""); +} diff --git a/lib/new-ui/widgets/floating_amount_input.dart b/lib/new-ui/widgets/floating_amount_input.dart new file mode 100644 index 000000000..3367d0a3a --- /dev/null +++ b/lib/new-ui/widgets/floating_amount_input.dart @@ -0,0 +1,92 @@ +import 'package:cw_core/currency.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class FloatingAmountInput extends StatefulWidget { + const FloatingAmountInput({super.key, required this.currency, required this.controller, this.focusNode, this.inputFormatters, this.onChanged, this.validator}); + + final Currency currency; + final TextEditingController controller; + final FocusNode? focusNode; + final List? inputFormatters; + final Function(String)? onChanged; + final FormFieldValidator? validator; + + @override + State createState() => _FloatingAmountInputState(); +} + +class _FloatingAmountInputState extends State { + bool _amountFocused = false; + late FocusNode focusNode = widget.focusNode ?? FocusNode(); + + @override + void initState() { + super.initState(); + focusNode.addListener(() => setState(() => _amountFocused = focusNode.hasFocus)); + } + + @override + Widget build(BuildContext context) { + return Center( + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + IntrinsicWidth( + child: TextFormField( + controller: widget.controller, + focusNode: focusNode, + maxLines: 1, + onChanged: widget.onChanged, + autovalidateMode: AutovalidateMode.always, + validator: widget.validator, + keyboardType: TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + inputFormatters: [ + FilteringTextInputFormatter.allow( + RegExp(r'^\d*[.,]?\d*$'), + ), + ], + decoration: InputDecoration( + isDense: true, + isCollapsed: true, + contentPadding: EdgeInsets.zero, + fillColor: Colors.transparent, + hoverColor: Colors.transparent, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + hintText: _amountFocused || widget.controller.text.isNotEmpty + ? null + : "0.00", + hintStyle: Theme.of(context).textTheme.displayMedium?.copyWith( + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + style: Theme.of(context).textTheme.displayMedium?.copyWith( + fontWeight: FontWeight.w400, + fontSize: 45, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + const SizedBox(width: 8), + Text( + widget.currency.symbol, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.displayMedium?.copyWith( + fontWeight: FontWeight.w400, + fontSize: 45, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/screens/buy/buy_sell_page.dart b/lib/src/screens/buy/buy_sell_page.dart index 73c238b57..9c05309c6 100644 --- a/lib/src/screens/buy/buy_sell_page.dart +++ b/lib/src/screens/buy/buy_sell_page.dart @@ -3,6 +3,7 @@ import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart'; import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart'; import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart'; import 'package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart'; @@ -412,7 +413,7 @@ class BuySellPage extends BasePage { borderColor: Theme.of(context).colorScheme.outlineVariant, onPushPasteButton: (context) async {}, onPushAddressBookButton: (context) async {}, - fillColor: buySellViewModel.isBuyAction + fillColor: buySellViewModel.mode == BuySellPageMode.buy ? Theme.of(context).colorScheme.surfaceContainer : Theme.of(context).colorScheme.surfaceContainerLow, ), @@ -451,7 +452,7 @@ class BuySellPage extends BasePage { addressTextFieldValidator: AddressValidator(type: buySellViewModel.cryptoCurrency), onPushPasteButton: (context) async {}, onPushAddressBookButton: (context) async {}, - fillColor: buySellViewModel.isBuyAction + fillColor: buySellViewModel.mode == BuySellPageMode.buy ? Theme.of(context).colorScheme.surfaceContainerLow : Theme.of(context).colorScheme.surfaceContainer, useSatoshis: buySellViewModel.useSatoshi, @@ -461,50 +462,14 @@ class BuySellPage extends BasePage { if (responsiveLayoutUtil.shouldRenderMobileUI) { return Observer( builder: (_) { - if (buySellViewModel.isBuyAction) { - return MobileExchangeCardsSection( - firstExchangeCard: fiatExchangeCard, - secondExchangeCard: cryptoExchangeCard, - onBuyTap: () => null, - onSellTap: () => - buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null, - isBuySellOption: true, - ); - } else { - return MobileExchangeCardsSection( - firstExchangeCard: cryptoExchangeCard, - secondExchangeCard: fiatExchangeCard, - onBuyTap: () => - !buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null, - onSellTap: () => null, - isBuySellOption: true, - ); - } + return Placeholder(); }, ); } return Observer( builder: (_) { - if (buySellViewModel.isBuyAction) { - return DesktopExchangeCardsSection( - firstExchangeCard: fiatExchangeCard, - secondExchangeCard: cryptoExchangeCard, - onBuyTap: () => null, - onSellTap: () => - buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null, - isBuySellOption: true, - ); - } else { - return DesktopExchangeCardsSection( - firstExchangeCard: cryptoExchangeCard, - secondExchangeCard: fiatExchangeCard, - onBuyTap: () => - !buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null, - onSellTap: () => null, - isBuySellOption: true, - ); - } + return Placeholder(); }, ); } diff --git a/lib/view_model/buy/buy_sell_view_model.dart b/lib/view_model/buy/buy_sell_view_model.dart index 4d181d7f6..100f13329 100644 --- a/lib/view_model/buy/buy_sell_view_model.dart +++ b/lib/view_model/buy/buy_sell_view_model.dart @@ -11,10 +11,13 @@ import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/provider_types.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart'; import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/buy/buy_sell_page.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:flutter/cupertino.dart'; import 'package:mobx/mobx.dart'; @@ -25,6 +28,7 @@ class BuySellViewModel = BuySellViewModelBase with _$BuySellViewModel; abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with Store { BuySellViewModelBase( AppStore appStore, + {required this.mode} ) : _cryptoAmount = '', fiatAmount = '', cryptoCurrencyAddress = '', @@ -81,7 +85,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S final formattedFiatAmount = double.tryParse(fiatAmount); final formattedCryptoAmount = double.tryParse(_cryptoAmount); - return isBuyAction + return mode == BuySellPageMode.buy ? formattedFiatAmount ?? 200.0 : formattedCryptoAmount ?? (cryptoCurrency == CryptoCurrency.btc ? 0.001 : 1); } @@ -100,8 +104,8 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S @observable List fiatCurrencies; - @observable - bool isBuyAction = true; + final BuySellPageMode mode; + @observable List providerList; @@ -234,11 +238,6 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S _initialize(); } - @action - void changeBuySellAction() { - isBuyAction = !isBuyAction; - _initialize(); - } @action void changeFiatCurrency({required FiatCurrency currency}) { @@ -267,16 +266,18 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S } if (!isReadyToTrade && !isBuySellQuoteFailed) { - _cryptoAmount = S.current.fetching; + _cryptoAmount = "..."; return; } else if (isBuySellQuoteFailed) { _cryptoAmount = ''; return; } + printV(bestRateQuote); if (bestRateQuote != null) { final enteredAmount = double.tryParse(fiatAmount.replaceAll(',', '.')) ?? 0; final amount = enteredAmount / bestRateQuote!.rate; + printV(amount); _cryptoAmount = amount.toString().withMaxDecimals(cryptoCurrency.decimals); } else { @@ -391,7 +392,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S } void _setProviders() => - providerList = isBuyAction ? availableBuyProviders : availableSellProviders; + providerList = mode == BuySellPageMode.buy ? availableBuyProviders : availableSellProviders; Future _initialize() async { _setProviders(); @@ -420,7 +421,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S paymentMethodState = PaymentMethodLoading(); selectedPaymentMethod = null; final result = await Future.wait(providerList.map((element) => - element.getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency, isBuyAction).timeout( + element.getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency, mode == BuySellPageMode.buy).timeout( Duration(seconds: 10), onTimeout: () => [], ))); @@ -456,7 +457,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S buySellQuotState = BuySellQuotLoading(); final List validProviders = providerList.where((provider) { - if (isBuyAction) { + if (mode == BuySellPageMode.buy) { return provider.supportedCryptoList .any((pair) => pair.from == cryptoCurrency && pair.to == fiatCurrency); } else { @@ -476,7 +477,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S fiatCurrency: fiatCurrency, amount: amount, paymentType: selectedPaymentMethod?.paymentMethodType, - isBuyAction: isBuyAction, + isBuyAction: mode == BuySellPageMode.buy, walletAddress: wallet.walletAddresses.address, customPaymentMethodType: selectedPaymentMethod?.customPaymentMethodType, ) @@ -498,7 +499,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S return; } - if (isBuyAction) { + if (mode == BuySellPageMode.buy) { validQuotes.sort((a, b) => b.payout.compareTo(a.payout)); } else { validQuotes.sort((a, b) => a.payout.compareTo(b.payout)); @@ -541,7 +542,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S final Quote effectiveBestRateQuote = sortedRecommendedQuotes.reduce((a, b) { - return isBuyAction ? a.rate < b.rate ? a : b : a.rate > b.rate ? a : b; + return mode == BuySellPageMode.buy ? a.rate < b.rate ? a : b : a.rate > b.rate ? a : b; }); @@ -563,7 +564,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S context: context, quote: selectedQuote!, amount: amount, - isBuyAction: isBuyAction, + isBuyAction: mode == BuySellPageMode.buy, cryptoCurrencyAddress: cryptoCurrencyAddress, ); } catch (e) {