From 5264eed642f46644e5274348bb6ce62db4d9bab9 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Tue, 30 Jun 2026 21:07:00 +0200 Subject: [PATCH 01/11] new buy/sell flow (wip) --- .../buy_sell/buy_sell_selector_modal.dart | 94 +++++++++++++++++++ .../widgets/coins_page/cards/cards_view.dart | 5 +- res/pictures/plus.svg | 3 + res/pictures/sell.svg | 3 + res/values/strings_en.arb | 6 ++ 5 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart create mode 100644 res/pictures/plus.svg create mode 100644 res/pictures/sell.svg diff --git a/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart b/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart new file mode 100644 index 0000000000..88ddcf9861 --- /dev/null +++ b/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart @@ -0,0 +1,94 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/modal_navigator.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; +import 'package:cake_wallet/themes/core/theme_extension.dart'; +import 'package:flutter/material.dart'; + +enum BuySellPageMode { buy, sell } + +class BuySellSelectorModal extends StatelessWidget { + const BuySellSelectorModal({super.key}); + + @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( + top: false, + child: Column(spacing:24, mainAxisSize: MainAxisSize.min, children: [ + SizedBox.shrink(), + Text(S.of(context).buy_or_sell, style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500),), + Text(S.of(context).buy_or_sell_desc, style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),), + Padding( + padding: EdgeInsets.symmetric(horizontal: 18.0), + child: Column(spacing: 12,children: [ + BuySellSelectorModalButton(title: S.of(context).buy_crypto, description: S.of(context).buy_crypto_desc, iconPath: "assets/new-ui/plus.svg", onTap: ()=>openBuySellPage(context, BuySellPageMode.buy),), + BuySellSelectorModalButton(title: S.of(context).sell_crypto, description: S.of(context).sell_crypto_desc, iconPath: "assets/new-ui/sell.svg", onTap: ()=>openBuySellPage(context, BuySellPageMode.sell)) + ],), + ), + SizedBox.shrink() + ]) + ) + ); + } + + void openBuySellPage(BuildContext context, BuySellPageMode mode) { + Navigator.of(context).pop(); + // showModalBottomSheet(context: context, builder: ModalNavigator(rootPage: ,)) + } +} + + +class BuySellSelectorModalButton extends StatelessWidget { + const BuySellSelectorModalButton({super.key, required this.title, required this.description, required this.iconPath, required this.onTap}); + + final String title; + final String description; + final String iconPath; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all( + width: 1, color: Theme.of(context).colorScheme.surfaceContainerHigh, + + ), + gradient: LinearGradient( + colors: [ + context.customColors.cardGradientColorPrimary, + context.customColors.cardGradientColorSecondary + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Padding(padding: EdgeInsets.all(24), child: Row(spacing: 20, children: [ + + CakeImageWidget(imageUrl: iconPath, width: 55, height: 55, colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant,BlendMode.srcIn),), + Column(spacing: 8, crossAxisAlignment: CrossAxisAlignment.start,children: [ + Text(title, style: TextStyle(fontWeight: FontWeight.w500, fontSize: 16),), + Text(description, style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),) + ],) + + ],),), + + ), + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/cards/cards_view.dart b/lib/new-ui/widgets/coins_page/cards/cards_view.dart index 238f20502c..ca6cb6bb8c 100644 --- a/lib/new-ui/widgets/coins_page/cards/cards_view.dart +++ b/lib/new-ui/widgets/coins_page/cards/cards_view.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/new-ui/modal_navigator.dart'; import 'package:cake_wallet/new-ui/pages/send_page.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/utils/feature_flag.dart'; import 'package:cake_wallet/utils/payment_request.dart'; @@ -177,7 +178,9 @@ class _CardsViewState extends State { label: S.current.buy, icon: Icons.arrow_forward_ios_rounded, iconSize: 12, - onTap: () => Navigator.of(context).pushNamed(Routes.buySellPage), + onTap: () { + showModalBottomSheet(context: context, builder: (context)=>BuySellSelectorModal()); + }, ) ] : []; diff --git a/res/pictures/plus.svg b/res/pictures/plus.svg new file mode 100644 index 0000000000..e6b5f64e3c --- /dev/null +++ b/res/pictures/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/pictures/sell.svg b/res/pictures/sell.svg new file mode 100644 index 0000000000..50c8271e77 --- /dev/null +++ b/res/pictures/sell.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 1debd0a8fc..8cf1c7835b 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -135,7 +135,11 @@ "buy": "Buy", "buy_alert_content": "Currently we only support the purchase of Bitcoin, Ethereum, Litecoin, and Monero. Please create or switch to your Bitcoin, Ethereum, Litecoin, or Monero wallet.", "buy_bitcoin": "Buy Bitcoin", + "buy_crypto": "Buy Crypto", + "buy_crypto_desc": "Fund your wallet with fiat", "buy_now": "Buy Now", + "buy_or_sell": "Buy or Sell", + "buy_or_sell_desc": "Exchange fiat for crypto thanks to our providers", "buy_provider_unavailable": "Provider currently unavailable.", "buy_sell_pair_is_not_supported_warning": "This currency pair isn’t supported by any provider for the selected payment method. Please choose a different pair or try changing the payment method.", "buy_with": "Buy with", @@ -988,6 +992,8 @@ "selected_trocador_provider": "selected Trocador provider", "sell": "Sell", "sell_alert_content": "We currently only support the sale of Bitcoin, Ethereum and Litecoin. Please create or switch to your Bitcoin, Ethereum or Litecoin wallet.", + "sell_crypto": "Sell Crypto", + "sell_crypto_desc": "Get fiat for your crypto", "sell_monero_com_alert_content": "Selling Monero is not supported yet", "send": "Send", "send_address": "${cryptoCurrency} address", From 0c469f20900b36e6b31ffa16ac776897592a4e8a Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Tue, 30 Jun 2026 22:06:47 +0200 Subject: [PATCH 02/11] wip --- lib/buy/buy_provider.dart | 3 +- lib/di.dart | 5 ++ .../pages/buy_sell/buy_sell_amount_page.dart | 50 +++++++++++++++++++ .../buy_sell/buy_sell_selector_modal.dart | 5 +- 4 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 lib/new-ui/pages/buy_sell/buy_sell_amount_page.dart diff --git a/lib/buy/buy_provider.dart b/lib/buy/buy_provider.dart index 7cc92eb9b1..5053a7ef85 100644 --- a/lib/buy/buy_provider.dart +++ b/lib/buy/buy_provider.dart @@ -43,8 +43,7 @@ abstract class BuyProvider { required double amount, required bool isBuyAction, required String cryptoCurrencyAddress, - String? countryCode}) => - null; + String? countryCode}); Future requestUrl(String amount, String sourceCurrency) => throw UnimplementedError(); diff --git a/lib/di.dart b/lib/di.dart index 821f51130a..a4ae25ca97 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -56,6 +56,7 @@ import 'package:cake_wallet/new-ui/pages/account_customizer.dart'; import 'package:cake_wallet/new-ui/pages/bridge/bridge_amount_page.dart'; import 'package:cake_wallet/new-ui/pages/bridge/bridge_network_page.dart'; import 'package:cake_wallet/new-ui/pages/bridge/bridge_receiving_wallet_page.dart'; +import 'package:cake_wallet/new-ui/pages/buy_sell/buy_sell_amount_page.dart'; import 'package:cake_wallet/new-ui/pages/coin_control_page.dart'; import 'package:cake_wallet/new-ui/pages/addresses_page.dart'; import 'package:cake_wallet/new-ui/pages/home_page.dart'; @@ -64,6 +65,7 @@ import 'package:cake_wallet/new-ui/pages/lightning_username_page.dart'; import 'package:cake_wallet/new-ui/pages/receive_page.dart'; import 'package:cake_wallet/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart'; import 'package:cake_wallet/new-ui/widgets/addresses_page/address_label_input.dart'; +import 'package:cake_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart'; import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart'; import 'package:cake_wallet/new-ui/widgets/receive_page/receive_label_modal.dart'; import 'package:cake_wallet/new-ui/pages/swap_page.dart'; @@ -1494,6 +1496,9 @@ Future setup({ getIt.registerFactory(() => BuySellViewModel(getIt.get())); + getIt.registerFactoryParam((mode, _) => + NewBuySellAmountPage(mode: mode, buySellViewModel: getIt.get())); + getIt.registerFactory(() => BuySellPage(getIt.get())); getIt.registerFactoryParam, void>((List args, _) { 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 new file mode 100644 index 0000000000..b29a747dba --- /dev/null +++ b/lib/new-ui/pages/buy_sell/buy_sell_amount_page.dart @@ -0,0 +1,50 @@ +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 NewBuySellAmountPage extends StatelessWidget { + const NewBuySellAmountPage({super.key, required this.mode, required this.buySellViewModel}); + + final BuySellPageMode mode; + 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.close), onLeadingPressed: Navigator.of(context).pop,), + Expanded(child: GridView.builder(itemCount: 6, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + mainAxisExtent: 150), itemBuilder: (context, index){ + + + })) + ],), + ), + ); + } + + String get _pageTitle => + mode == BuySellPageMode.buy ? S.current.buy : S.current.sell + + ((buySellViewModel.cryptoCurrencies.length == 1) + ? " ${buySellViewModel.cryptoCurrencies.first.fullName}" + : ""); +} + + diff --git a/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart b/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart index 88ddcf9861..85b82566ad 100644 --- a/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart +++ b/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart @@ -1,6 +1,7 @@ +import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/new-ui/modal_navigator.dart'; -import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/new-ui/pages/buy_sell/buy_sell_amount_page.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/themes/core/theme_extension.dart'; import 'package:flutter/material.dart'; @@ -45,7 +46,7 @@ class BuySellSelectorModal extends StatelessWidget { void openBuySellPage(BuildContext context, BuySellPageMode mode) { Navigator.of(context).pop(); - // showModalBottomSheet(context: context, builder: ModalNavigator(rootPage: ,)) + showModalBottomSheet(isScrollControlled: true, context: context, builder: (modalContext)=>ModalNavigator(rootPage: getIt.get(param1: mode), parentContext: context,)); } } From a2a0500213788270016e221ff779b8b08fe55846 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Wed, 1 Jul 2026 09:56:20 +0200 Subject: [PATCH 03/11] wip --- lib/buy/buy_provider.dart | 2 +- .../pages/buy_sell/buy_sell_amount_page.dart | 168 ++++++++++++++++-- .../buy_sell/buy_sell_selector_modal.dart | 2 +- lib/view_model/buy/buy_sell_view_model.dart | 53 ++++++ res/values/strings_en.arb | 2 + 5 files changed, 209 insertions(+), 18 deletions(-) diff --git a/lib/buy/buy_provider.dart b/lib/buy/buy_provider.dart index 5053a7ef85..d2a4bc9661 100644 --- a/lib/buy/buy_provider.dart +++ b/lib/buy/buy_provider.dart @@ -43,7 +43,7 @@ abstract class BuyProvider { required double amount, required bool isBuyAction, required String cryptoCurrencyAddress, - String? countryCode}); + String? countryCode}) => null; Future requestUrl(String amount, String sourceCurrency) => throw UnimplementedError(); 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 b29a747dba..3b36c5834e 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,15 +1,25 @@ +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/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:flutter/material.dart'; -class NewBuySellAmountPage extends StatelessWidget { +class NewBuySellAmountPage extends StatefulWidget { const NewBuySellAmountPage({super.key, required this.mode, required this.buySellViewModel}); final BuySellPageMode mode; final BuySellViewModel buySellViewModel; + @override + State createState() => _NewBuySellAmountPageState(); +} + +class _NewBuySellAmountPageState extends State { + bool _customAmountMode = false; + @override Widget build(BuildContext context) { return Container( @@ -25,26 +35,152 @@ class NewBuySellAmountPage extends StatelessWidget { ), ), child: SafeArea( - child: Column(children: [ - ModalTopBar(title: _pageTitle, leadingIcon: Icon(Icons.close), onLeadingPressed: Navigator.of(context).pop,), - Expanded(child: GridView.builder(itemCount: 6, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 10, - mainAxisSpacing: 10, - mainAxisExtent: 150), itemBuilder: (context, index){ - - - })) - ],), + child: Column( + children: [ + ModalTopBar( + title: _pageTitle, + leadingIcon: Icon(Icons.close), + onLeadingPressed: Navigator.of(context).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); + } + }, + )) + ], + ), ), ); } - String get _pageTitle => - mode == BuySellPageMode.buy ? S.current.buy : S.current.sell + - ((buySellViewModel.cryptoCurrencies.length == 1) - ? " ${buySellViewModel.cryptoCurrencies.first.fullName}" + String get _pageTitle => widget.mode == BuySellPageMode.buy + ? S.current.buy + : S.current.sell + + ((widget.buySellViewModel.cryptoCurrencies.length == 1) + ? " ${widget.buySellViewModel.cryptoCurrencies.first.fullName}" : ""); } +class BuySellDefaultAmountSelector extends StatelessWidget { + const BuySellDefaultAmountSelector( + {super.key, + required this.defaultAmounts, + required this.currency, + required this.mode, + required this.onSelected}); + + final List defaultAmounts; + final FiatCurrency currency; + final BuySellPageMode mode; + final Function(String?) onSelected; + + @override + Widget build(BuildContext context) { + return Column( + spacing: 24, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + mode == BuySellPageMode.sell + ? S.of(context).choose_amount_to_sell + : S.of(context).choose_amount_to_buy, + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: GridView.builder( + shrinkWrap: true, + // +1 for "custom" option + itemCount: defaultAmounts.length + 1, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, crossAxisSpacing: 8, mainAxisSpacing: 16, mainAxisExtent: 105), + itemBuilder: (context, index) { + final String? item = index == defaultAmounts.length ? null : defaultAmounts[index]; + + return BuySellAmountPill( + amount: item == null ? null : Money.parse(item, currency), + onTap: () => onSelected(item), + ); + }), + ), + ], + ); + } +} + +class BuySellAmountPill extends StatelessWidget { + const BuySellAmountPill({super.key, this.amount, required this.onTap}); + + final Money? amount; + final VoidCallback onTap; + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(9999999999), + border: Border.all( + width: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + gradient: LinearGradient( + colors: [ + context.customColors.cardGradientColorPrimary, + context.customColors.cardGradientColorSecondary + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(9999999999), + child: InkWell( + borderRadius: BorderRadius.circular(9999999999), + onTap: onTap, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + spacing: 4, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (amount != null) + Text( + amount!.toStringWithPrecision(fractionalDigits: 0), + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), + ), + Text( + amount?.currency.symbol ?? S.of(context).custom, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: amount == null + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurfaceVariant), + ) + ], + ), + if (amount == null) + Text( + S.of(context).enter_amount, + style: TextStyle( + fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart b/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart index 85b82566ad..7e6efd0044 100644 --- a/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart +++ b/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart @@ -46,7 +46,7 @@ class BuySellSelectorModal extends StatelessWidget { void openBuySellPage(BuildContext context, BuySellPageMode mode) { Navigator.of(context).pop(); - showModalBottomSheet(isScrollControlled: true, context: context, builder: (modalContext)=>ModalNavigator(rootPage: getIt.get(param1: mode), parentContext: context,)); + showModalBottomSheet(useSafeArea:true, isScrollControlled: true, context: context, builder: (modalContext)=>ModalNavigator(rootPage: getIt.get(param1: mode), parentContext: context,)); } } diff --git a/lib/view_model/buy/buy_sell_view_model.dart b/lib/view_model/buy/buy_sell_view_model.dart index a299aebac8..4d181d7f63 100644 --- a/lib/view_model/buy/buy_sell_view_model.dart +++ b/lib/view_model/buy/buy_sell_view_model.dart @@ -149,6 +149,59 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S @observable bool skipIsReadyToTradeReaction = false; + + + // based on usd values, should have roughly equal worth (was done with ai though so it's subject to correction) + static final Map> _defaultAmountsMap = { + FiatCurrency.amd: ["20000", "40000", "200000", "400000", "1000000"], + FiatCurrency.aud: ["100", "200", "1000", "2000", "5000"], + FiatCurrency.bgn: ["100", "200", "1000", "2000", "5000"], + FiatCurrency.brl: ["250", "500", "2500", "5000", "12500"], + FiatCurrency.cad: ["50", "100", "500", "1000", "2500"], + FiatCurrency.chf: ["50", "100", "500", "1000", "2500"], + FiatCurrency.clp: ["50000", "100000", "500000", "1000000", "2500000"], + FiatCurrency.cop: ["200000", "400000", "2000000", "4000000", "10000000"], + FiatCurrency.czk: ["1000", "2000", "10000", "20000", "50000"], + FiatCurrency.dkk: ["400", "800", "4000", "8000", "20000"], + FiatCurrency.egp: ["2500", "5000", "25000", "50000", "125000"], + FiatCurrency.eur: ["50", "100", "500", "1000", "2500"], + FiatCurrency.gbp: ["50", "100", "500", "1000", "2500"], + FiatCurrency.gtq: ["400", "800", "4000", "8000", "20000"], + FiatCurrency.hkd: ["400", "800", "4000", "8000", "20000"], + FiatCurrency.hrk: ["400", "800", "4000", "8000", "20000"], + FiatCurrency.huf: ["20000", "40000", "200000", "400000", "1000000"], + FiatCurrency.idr: ["800000", "1600000", "8000000", "16000000", "40000000"], + FiatCurrency.ils: ["200", "400", "2000", "4000", "10000"], + FiatCurrency.inr: ["5000", "10000", "50000", "100000", "250000"], + FiatCurrency.isk: ["7000", "14000", "70000", "140000", "350000"], + FiatCurrency.jpy: ["10000", "20000", "100000", "200000", "500000"], + FiatCurrency.krw: ["50000", "100000", "500000", "1000000", "2500000"], + FiatCurrency.mad: ["500", "1000", "5000", "10000", "25000"], + FiatCurrency.mxn: ["1000", "2000", "10000", "20000", "50000"], + FiatCurrency.myr: ["250", "500", "2500", "5000", "12500"], + FiatCurrency.ngn: ["50000", "100000", "500000", "1000000", "2500000"], + FiatCurrency.nok: ["500", "1000", "5000", "10000", "25000"], + FiatCurrency.nzd: ["100", "200", "1000", "2000", "5000"], + FiatCurrency.php: ["3000", "6000", "30000", "60000", "150000"], + FiatCurrency.pkr: ["15000", "30000", "150000", "300000", "750000"], + FiatCurrency.pln: ["200", "400", "2000", "4000", "10000"], + FiatCurrency.ron: ["250", "500", "2500", "5000", "12500"], + FiatCurrency.sek: ["500", "1000", "5000", "10000", "25000"], + FiatCurrency.sgd: ["50", "100", "500", "1000", "2500"], + FiatCurrency.thb: ["2000", "4000", "20000", "40000", "100000"], + FiatCurrency.tur: ["1500", "3000", "15000", "30000", "75000"], + FiatCurrency.twd: ["1500", "3000", "15000", "30000", "75000"], + FiatCurrency.usd: ["50", "100", "500", "1000", "2500"], + FiatCurrency.vnd: ["1000000", "2000000", "10000000", "20000000", "50000000"], + FiatCurrency.zar: ["1000", "2000", "10000", "20000", "50000"], + FiatCurrency.kes: ["5000", "10000", "50000", "100000", "250000"], + }; + + // the fallback is just the usd values. + // not great but this fallback shouldn't be triggered anyway + List get defaultAmounts => + _defaultAmountsMap[fiatCurrency] ?? ["50", "100", "500", "1000", "2500"]; + @computed bool get isReadyToTrade { final hasSelectedQuote = selectedQuote != null; diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 8cf1c7835b..207ee288cf 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -199,6 +199,8 @@ "choose_a_provider": "Choose a provider", "choose_account": "Choose account", "choose_address": "\n\nPlease choose the address:", + "choose_amount_to_buy": "Choose amount to buy", + "choose_amount_to_sell": "Choose amount to sell", "choose_card_value": "Choose a card value", "choose_derivation": "Choose Wallet Derivation", "choose_from_available_options": "Choose from the available options:", From afbc6db06621dd8a0d67153f2d44bff4d7f628b5 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Wed, 1 Jul 2026 16:49:19 +0200 Subject: [PATCH 04/11] 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 a4ae25ca97..faf6196af4 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 3b36c5834e..3270089386 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 0000000000..acbc7f71e2 --- /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 0000000000..3367d0a3ad --- /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 73c238b577..9c05309c6e 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 4d181d7f63..100f13329e 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) { From 72c2899195f657580d049208ce4438e4fba01717 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Fri, 3 Jul 2026 12:54:07 +0200 Subject: [PATCH 05/11] new buy/sell --- assets/images/skrill.svg | 4 +- lib/buy/buy_quote.dart | 5 +- lib/buy/payment_method.dart | 20 +- lib/di.dart | 2 +- .../list_item/list_item_regular_row.dart | 4 + .../pages/buy_sell/buy_sell_amount_page.dart | 303 ++++++++++++++---- .../buy_sell/buy_sell_confirmation_page.dart | 131 ++++++++ .../buy_sell_payment_method_page.dart | 71 ++++ .../buy_sell/buy_sell_provider_page.dart | 113 ++++++- .../buy_sell/buy_sell_redirecting_page.dart | 125 ++++++++ .../buy_sell/buy_sell_selector_modal.dart | 10 +- .../widgets/receive_page/receive_top_bar.dart | 25 +- lib/src/widgets/cake_image_widget.dart | 5 +- .../list_item_dropdown_widget.dart | 42 ++- .../list_item_regular_row_widget.dart | 22 +- .../new_list_row/new_list_section.dart | 2 + lib/view_model/buy/buy_sell_view_model.dart | 58 +++- pubspec_base.yaml | 1 + .../buy_payment_methods/all_methods.svg | 3 + .../buy_payment_methods/apple_pay.svg | 8 + .../buy_payment_methods/bank_transfer.svg | 3 + .../buy_payment_methods/debit_card.svg | 3 + .../buy_payment_methods/google_pay.svg | 11 + res/pictures/buy_payment_methods/paypal.svg | 68 ++++ res/values/strings_en.arb | 12 + 25 files changed, 926 insertions(+), 125 deletions(-) create mode 100644 lib/new-ui/pages/buy_sell/buy_sell_confirmation_page.dart create mode 100644 lib/new-ui/pages/buy_sell/buy_sell_payment_method_page.dart create mode 100644 lib/new-ui/pages/buy_sell/buy_sell_redirecting_page.dart create mode 100644 res/pictures/buy_payment_methods/all_methods.svg create mode 100644 res/pictures/buy_payment_methods/apple_pay.svg create mode 100644 res/pictures/buy_payment_methods/bank_transfer.svg create mode 100644 res/pictures/buy_payment_methods/debit_card.svg create mode 100644 res/pictures/buy_payment_methods/google_pay.svg create mode 100644 res/pictures/buy_payment_methods/paypal.svg diff --git a/assets/images/skrill.svg b/assets/images/skrill.svg index b264b57eb9..542c00c129 100644 --- a/assets/images/skrill.svg +++ b/assets/images/skrill.svg @@ -1,9 +1,9 @@ - + - + diff --git a/lib/buy/buy_quote.dart b/lib/buy/buy_quote.dart index 1e154de6bf..ee301e82fa 100644 --- a/lib/buy/buy_quote.dart +++ b/lib/buy/buy_quote.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/provider_types.dart'; import 'package:cake_wallet/exchange/limits.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cw_core/crypto_currency.dart'; enum ProviderRecommendation { bestRate, lowKyc, successRate } @@ -13,11 +14,11 @@ extension RecommendationTitle on ProviderRecommendation { String get title { switch (this) { case ProviderRecommendation.bestRate: - return 'BEST RATE'; + return S.current.best_rate; case ProviderRecommendation.lowKyc: return 'LOW KYC'; case ProviderRecommendation.successRate: - return 'HIGHEST SUCCESS RATE'; + return S.current.highest_success_rate; } } } diff --git a/lib/buy/payment_method.dart b/lib/buy/payment_method.dart index 06f1cfe34f..b6b23093da 100644 --- a/lib/buy/payment_method.dart +++ b/lib/buy/payment_method.dart @@ -132,17 +132,21 @@ extension PaymentTypeTitle on PaymentType { String? get darkIconPath { switch (this) { case PaymentType.all: - return 'assets/images/usd_round_dark.svg'; + return 'assets/new-ui/buy_payment_methods/all_methods.svg'; case PaymentType.creditCard: case PaymentType.debitCard: case PaymentType.yellowCardBankTransfer: - return 'assets/images/card_dark.svg'; + return 'assets/new-ui/buy_payment_methods/debit_card.svg'; case PaymentType.bankTransfer: - return 'assets/images/bank_dark.svg'; + return 'assets/new-ui/buy_payment_methods/bank_transfer.svg'; case PaymentType.skrill: return 'assets/images/skrill.svg'; case PaymentType.applePay: - return 'assets/images/apple_pay_round_dark.svg'; + return 'assets/new-ui/buy_payment_methods/apple_pay.svg'; + case PaymentType.googlePay: + return "assets/new-ui/buy_payment_methods/google_pay.svg"; + case PaymentType.paypal: + return "assets/new-ui/buy_payment_methods/paypal.svg"; case PaymentType.revolutPay: return 'assets/images/revolut_dark.svg'; default: @@ -150,6 +154,12 @@ extension PaymentTypeTitle on PaymentType { } } + bool get isMonochromeIcon => [ + "assets/new-ui/buy_payment_methods/all_methods.svg", + "assets/new-ui/buy_payment_methods/debit_card.svg", + "assets/new-ui/buy_payment_methods/bank_transfer.svg" + ].contains(darkIconPath); + String? get description { switch (this) { default: @@ -190,7 +200,7 @@ class PaymentMethod extends SelectableOption { return PaymentMethod( paymentMethodType: PaymentType.all, customTitle: 'All Payment Methods', - customIconPath: 'assets/images/dollar_coin.svg'); + customIconPath: 'assets/new-ui/buy_payment_methods/all_methods.svg'); } factory PaymentMethod.fromOnramperJson(Map json) { diff --git a/lib/di.dart b/lib/di.dart index faf6196af4..4439a68bd7 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1495,7 +1495,7 @@ Future setup({ getIt.registerFactory(() => BuyAmountViewModel()); getIt.registerFactoryParam( - (mode, _) => BuySellViewModel(mode: mode, getIt.get())); + (mode, _) => BuySellViewModel(mode: mode, getIt.get(), fiatConversionStore: getIt.get())); getIt.registerFactoryParam((mode, _) => NewBuySellAmountPage(buySellViewModel: getIt.get(param1: mode))); diff --git a/lib/entities/new_ui_entities/list_item/list_item_regular_row.dart b/lib/entities/new_ui_entities/list_item/list_item_regular_row.dart index 4660199ff4..bfa2571d8e 100644 --- a/lib/entities/new_ui_entities/list_item/list_item_regular_row.dart +++ b/lib/entities/new_ui_entities/list_item/list_item_regular_row.dart @@ -22,6 +22,8 @@ class ListItemRegularRow extends ListItem { this.leadingIconSize, this.badgeIconSize, this.iconColor, + this.secondaryLabel, + this.subtitleColor, }); final String? subtitle; @@ -31,10 +33,12 @@ class ListItemRegularRow extends ListItem { final String? badgeIconPath; final String? copyableText; final VoidCallback? onTap; + final String? secondaryLabel; final bool showArrow; final Widget? bottomWidget; final Widget? trailingWidget; final bool truncateTrailingText; + final Color? subtitleColor; final Color? foregroundColor; final double? trailingIconSize; final Widget? leadingIconErrorWidget; 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 3270089386..ec22421b1d 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,15 +1,27 @@ +import 'dart:async'; + +import 'package:cake_wallet/buy/payment_method.dart'; +import 'package:cake_wallet/buy/sell_buy_states.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/pages/buy_sell/buy_sell_provider_page.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/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/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/themes/core/theme_extension.dart'; +import 'package:cake_wallet/utils/show_pop_up.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/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; class NewBuySellAmountPage extends StatefulWidget { const NewBuySellAmountPage({super.key, required this.buySellViewModel}); @@ -22,7 +34,24 @@ class NewBuySellAmountPage extends StatefulWidget { class _NewBuySellAmountPageState extends State { bool _customAmountMode = false; + bool _isLoadingPaymentMethods = false; final customInputController = TextEditingController(); + final customInputFocusNode = FocusNode(); + + @override + void initState() { + super.initState(); + + // this is a hack for the "up to" display to show the actual upper limit. + // limits are loaded alongside quotes, and quotes for some providers only load properly with an amount and payment method selected. + // it'll be refactored soon anyway, i guess. + widget.buySellViewModel.changeFiatAmount(amount: "1000"); + widget.buySellViewModel.calculateBestRate(); + when( + (_) => widget.buySellViewModel.paymentMethodState is PaymentMethodLoaded, + () => widget.buySellViewModel.selectedPaymentMethod = widget.buySellViewModel.paymentMethods + .firstWhere((item) => item.paymentMethodType == PaymentType.all)); + } @override Widget build(BuildContext context) { @@ -41,40 +70,61 @@ class _NewBuySellAmountPageState extends State { child: SafeArea( child: Column( children: [ - ModalTopBar( - title: _pageTitle, - leadingIcon: Icon(Icons.close), - onLeadingPressed: Navigator.of(context, rootNavigator: true).pop, + Observer( + builder: (_) => ModalTopBar( + title: _pageTitle, + bottomText: !_customAmountMode || widget.buySellViewModel.maxFiatAmount == null + ? null + : "${S.of(context).up_to} ~${widget.buySellViewModel.maxFiatAmount} ${widget.buySellViewModel.fiatCurrency.title}", + leadingIcon: Icon(Icons.close), + onLeadingPressed: Navigator.of(context, rootNavigator: true).pop, + ), ), Expanded( - 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); - } - }, - ), + child: Observer( + builder: (_) => AnimatedSwitcher( + duration: Duration(milliseconds: 300), + child: _customAmountMode + ? BuySellCustomAmountInput( + fiatCurrency: widget.buySellViewModel.fiatCurrency, + cryptoCurrency: widget.buySellViewModel.cryptoCurrency, + cryptoAmount: widget.buySellViewModel.cryptoAmount, + isLoading: _isLoadingPaymentMethods, + hasCurrencySelector: widget.buySellViewModel.hasMultipleCurrencies, + onCurrencySelectorPressed: () => selectCryptoCurrency(context), + controller: customInputController, + focusNode: customInputFocusNode, + onContinuePressed: () { + navigateToProviders(context); + }, + onChanged: (amount) => + widget.buySellViewModel.changeFiatAmount(amount: amount), + ) + : BuySellDefaultAmountSelector( + key: ValueKey(0), + defaultAmounts: widget.buySellViewModel.defaultAmounts, + fiatCurrency: widget.buySellViewModel.fiatCurrency, + currentAmount: widget.buySellViewModel.fiatAmount, + hasCurrencySelector: widget.buySellViewModel.hasMultipleCurrencies, + onCurrencySelectorPressed: () => selectCryptoCurrency(context), + cryptoCurrency: widget.buySellViewModel.cryptoCurrency, + isLoading: _isLoadingPaymentMethods, + mode: widget.buySellViewModel.mode, + onSelected: (amount) async { + if (amount == null) { + // this resets the rate and prevents showing 0 usd = 0.something btc + await widget.buySellViewModel.changeFiatAmount(amount: ""); + setState(() { + _customAmountMode = true; + }); + customInputFocusNode.requestFocus(); + } else { + await widget.buySellViewModel.changeFiatAmount(amount: amount); + navigateToProviders(context); + } + }, + ), + ), )) ], ), @@ -82,12 +132,60 @@ class _NewBuySellAmountPageState extends State { ); } + void selectCryptoCurrency(BuildContext context) => CurrencyPickerSheet.show( + context: context, + args: CurrencyPickerArgs( + items: widget.buySellViewModel.activeWalletCurrencies.toList(), + onSelected: (item) => widget.buySellViewModel.changeCryptoCurrency(currency: item), + symbolResolver: widget.buySellViewModel.amountParsingProxy.getCryptoSymbol, + )); + String get _pageTitle => widget.buySellViewModel.mode == BuySellPageMode.buy ? S.current.buy : S.current.sell + ((widget.buySellViewModel.cryptoCurrencies.length == 1) ? " ${widget.buySellViewModel.cryptoCurrencies.first.fullName}" : ""); + + Future navigateToProviders(BuildContext context) async { + if (_isLoadingPaymentMethods) { + return; + } + + try { + setState(() { + _isLoadingPaymentMethods = true; + }); + + await asyncWhen((_) => [PaymentMethodLoaded, PaymentMethodFailed] + .contains(widget.buySellViewModel.paymentMethodState.runtimeType)); + + if (widget.buySellViewModel.paymentMethodState is PaymentMethodFailed) { + showPopUp( + context: context, + builder: (context) => AlertWithOneAction( + alertTitle: S.of(context).failed_to_load_payment_methods, + alertContent: S.of(context).please_try_again_later, + buttonText: "OK", + buttonAction: Navigator.of(context).pop)); + return; + } + + widget.buySellViewModel.selectedPaymentMethod = widget.buySellViewModel.paymentMethods + .firstWhere((item) => item.paymentMethodType == PaymentType.all); + + // unawaited because BuySellProviderPage has a nice little "loading rates..." ui thingie + unawaited(widget.buySellViewModel.calculateBestRate()); + + final page = BuySellProviderPage(buySellViewModel: widget.buySellViewModel); + Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => Material(color: Colors.transparent, child: page))); + } finally { + setState(() { + _isLoadingPaymentMethods = false; + }); + } + } } class BuySellCustomAmountInput extends StatelessWidget { @@ -98,12 +196,20 @@ class BuySellCustomAmountInput extends StatelessWidget { required this.cryptoAmount, required this.controller, required this.onContinuePressed, - required this.onChanged}); + required this.isLoading, + required this.onChanged, + required this.focusNode, + required this.hasCurrencySelector, + required this.onCurrencySelectorPressed}); final FiatCurrency fiatCurrency; final CryptoCurrency cryptoCurrency; final String cryptoAmount; final TextEditingController controller; + final FocusNode focusNode; + final bool isLoading; + final bool hasCurrencySelector; + final VoidCallback onCurrencySelectorPressed; final VoidCallback onContinuePressed; final Function(String) onChanged; @@ -116,8 +222,11 @@ class BuySellCustomAmountInput extends StatelessWidget { Column( spacing: 8, children: [ + BuySellCurrencyPickerPill(curr: cryptoCurrency, onTap: onCurrencySelectorPressed), + SizedBox.shrink(), FloatingAmountInput( currency: fiatCurrency, + focusNode: focusNode, controller: controller, onChanged: onChanged, ), @@ -137,6 +246,7 @@ class BuySellCustomAmountInput extends StatelessWidget { padding: const EdgeInsets.all(18.0), child: NewPrimaryButton( onPressed: onContinuePressed, + isLoading: isLoading, text: S.of(context).continue_text, color: Theme.of(context).colorScheme.primary, textColor: Theme.of(context).colorScheme.onPrimary), @@ -150,12 +260,22 @@ class BuySellDefaultAmountSelector extends StatelessWidget { const BuySellDefaultAmountSelector( {super.key, required this.defaultAmounts, - required this.currency, + required this.fiatCurrency, required this.mode, - required this.onSelected}); + required this.onSelected, + this.currentAmount, + required this.isLoading, + required this.hasCurrencySelector, + required this.onCurrencySelectorPressed, + required this.cryptoCurrency}); final List defaultAmounts; - final FiatCurrency currency; + final String? currentAmount; + final bool isLoading; + final bool hasCurrencySelector; + final VoidCallback onCurrencySelectorPressed; + final CryptoCurrency cryptoCurrency; + final FiatCurrency fiatCurrency; final BuySellPageMode mode; final Function(String?) onSelected; @@ -165,6 +285,7 @@ class BuySellDefaultAmountSelector extends StatelessWidget { spacing: 24, mainAxisAlignment: MainAxisAlignment.center, children: [ + BuySellCurrencyPickerPill(curr: cryptoCurrency, onTap: onCurrencySelectorPressed), Text( mode == BuySellPageMode.sell ? S.of(context).choose_amount_to_sell @@ -184,7 +305,8 @@ class BuySellDefaultAmountSelector extends StatelessWidget { final String? item = index == defaultAmounts.length ? null : defaultAmounts[index]; return BuySellAmountPill( - amount: item == null ? null : Money.parse(item, currency), + isLoading: isLoading && item == currentAmount, + amount: item == null ? null : Money.parse(item, fiatCurrency), onTap: () => onSelected(item), ); }), @@ -195,10 +317,11 @@ class BuySellDefaultAmountSelector extends StatelessWidget { } class BuySellAmountPill extends StatelessWidget { - const BuySellAmountPill({super.key, this.amount, required this.onTap}); + const BuySellAmountPill({super.key, this.amount, required this.onTap, required this.isLoading}); final Money? amount; final VoidCallback onTap; + final bool isLoading; @override Widget build(BuildContext context) { @@ -224,35 +347,85 @@ class BuySellAmountPill extends StatelessWidget { child: InkWell( borderRadius: BorderRadius.circular(9999999999), onTap: onTap, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - spacing: 4, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (amount != null) - Text( - amount!.toStringWithPrecision(fractionalDigits: 0), - style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), + child: isLoading + ? CupertinoActivityIndicator() + : Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + spacing: 4, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (amount != null) + Text( + amount!.toStringWithPrecision(fractionalDigits: 0), + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), + ), + Text( + amount?.currency.symbol ?? S.of(context).custom, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: amount == null + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurfaceVariant), + ) + ], ), - Text( - amount?.currency.symbol ?? S.of(context).custom, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: amount == null - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurfaceVariant), - ) - ], + if (amount == null) + Text( + S.of(context).enter_amount, + style: TextStyle( + fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), + ) + ], + ), + ), + ), + ); + } +} + +class BuySellCurrencyPickerPill extends StatelessWidget { + const BuySellCurrencyPickerPill({super.key, required this.curr, required this.onTap}); + + final CryptoCurrency curr; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(999999999), + ), + child: Padding( + padding: EdgeInsets.only(top: 5, bottom: 5, left: 5, right: 10), + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 10, + children: [ + CakeImageWidget( + imageUrl: curr.iconPath, + width: 30, + height: 30, + ), + Text( + curr.fullName ?? curr.title, + style: TextStyle(fontSize: 16), + ), + RotatedBox( + quarterTurns: 2, + child: CakeImageWidget( + imageUrl: "assets/new-ui/dropdown_arrow.svg", + width: 8, + height: 8, + colorFilter: + ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn), + ), ), - if (amount == null) - Text( - S.of(context).enter_amount, - style: TextStyle( - fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), - ) ], ), ), diff --git a/lib/new-ui/pages/buy_sell/buy_sell_confirmation_page.dart b/lib/new-ui/pages/buy_sell/buy_sell_confirmation_page.dart new file mode 100644 index 0000000000..5de696f7b1 --- /dev/null +++ b/lib/new-ui/pages/buy_sell/buy_sell_confirmation_page.dart @@ -0,0 +1,131 @@ +import 'package:cake_wallet/buy/payment_method.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/pages/buy_sell/buy_sell_redirecting_page.dart'; +import 'package:cake_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.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/src/widgets/cake_image_widget.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; +import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class BuySellConfirmationPage extends StatelessWidget { + const BuySellConfirmationPage({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: Observer( + builder: (_) { + return Column( + spacing: 24, + children: [ + Column( + spacing: 4, + children: [ + Text( + "${buySellViewModel.fiatAmount} ${buySellViewModel.fiatCurrency}", + style: TextStyle(fontSize: 32), + ), + Text( + "≈ ${buySellViewModel.amountForQuote(buySellViewModel.selectedQuote!).toStringWithSymbol(fractionalDigits: 8)}", + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w500), + ) + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: NewListSections(sections: { + "": [ + ListItemRegularRow( + keyValue: "provider", + label: S.of(context).provider, + trailingWidget: Row( + spacing: 8, + children: [ + CakeImageWidget( + imageUrl: buySellViewModel.selectedQuote!.darkIconPath, + width: 24, + height: 24, + ), + Text( + buySellViewModel.selectedQuote!.rampName ?? + buySellViewModel.selectedQuote!.provider.title, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w500), + ), + SizedBox.shrink() + ], + )), + ListItemRegularRow( + showArrow: false, + keyValue: "payment method", + label: S.of(context).payment_method, + trailingText: buySellViewModel.selectedQuote!.paymentType.title), + ListItemRegularRow( + showArrow: false, + keyValue: "rate", + label: S.of(context).rate, + trailingText: buySellViewModel.selectedQuote!.topLeftSubTitle) + ] + }), + ) + ], + ); + }, + )), + Padding( + padding: EdgeInsets.all(18), + child: NewPrimaryButton( + onPressed: () => confirm(context), + text: S.of(context).proceed, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary), + ) + ], + ), + ), + ); + } + + String get _pageTitle => + (buySellViewModel.mode == BuySellPageMode.buy ? S.current.buy : S.current.sell) + + " " + + (buySellViewModel.cryptoCurrency.fullName ?? ""); + + void confirm(BuildContext context) { + final page = BuySellRedirectingPage(buySellViewModel: buySellViewModel); + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context, rootNavigator: true).push(CupertinoPageRoute( + builder: (context) => Material( + child: page, + ))); + } +} diff --git a/lib/new-ui/pages/buy_sell/buy_sell_payment_method_page.dart b/lib/new-ui/pages/buy_sell/buy_sell_payment_method_page.dart new file mode 100644 index 0000000000..e1ca3f449a --- /dev/null +++ b/lib/new-ui/pages/buy_sell/buy_sell_payment_method_page.dart @@ -0,0 +1,71 @@ +import 'package:cake_wallet/buy/payment_method.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; +import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class BuySellPaymentMethodPage extends StatelessWidget { + const BuySellPaymentMethodPage({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: S.of(context).payment_method, + leadingIcon: Icon(Icons.arrow_back_ios_new), + onLeadingPressed: Navigator.of(context).pop, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Observer( + builder: (_) => NewListSections(sections: { + "": buySellViewModel.paymentMethods + .map((item) => ListItemRegularRow( + keyValue: item.title, + label: item.title, + showArrow: false, + iconPath: item.darkIconPath, + iconColor: item.paymentMethodType.isMonochromeIcon + ? Theme.of(context).colorScheme.onSurfaceVariant + : null, + trailingWidget: buySellViewModel.selectedPaymentMethod == item + ? Icon( + Icons.check, + color: Theme.of(context).colorScheme.primary, + size: 16, + ) + : null, + onTap: () async { + buySellViewModel.changeOption(item); + buySellViewModel.calculateBestRate(); + Navigator.of(context).pop(); + })) + .toList() + }), + ), + )) + ], + )), + ); + } +} 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 index acbc7f71e2..f965459513 100644 --- a/lib/new-ui/pages/buy_sell/buy_sell_provider_page.dart +++ b/lib/new-ui/pages/buy_sell/buy_sell_provider_page.dart @@ -1,14 +1,31 @@ +import 'package:cake_wallet/buy/buy_quote.dart'; +import 'package:cake_wallet/buy/sell_buy_states.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_dropdown.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/pages/buy_sell/buy_sell_confirmation_page.dart'; +import 'package:cake_wallet/new-ui/pages/buy_sell/buy_sell_payment_method_page.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/src/widgets/new_list_row/new_list_section.dart'; import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; -class BuySellProviderPage extends StatelessWidget { +class BuySellProviderPage extends StatefulWidget { const BuySellProviderPage({super.key, required this.buySellViewModel}); final BuySellViewModel buySellViewModel; + @override + State createState() => _BuySellProviderPageState(); +} + +class _BuySellProviderPageState extends State { + bool _allProvidersExpanded = false; + @override Widget build(BuildContext context) { return Container( @@ -31,15 +48,103 @@ class BuySellProviderPage extends StatelessWidget { leadingIcon: Icon(Icons.arrow_back_ios_new), onLeadingPressed: Navigator.of(context).pop, ), - Expanded(child: Column( + Expanded( + child: Observer( + builder: (_) { + if(widget.buySellViewModel.buySellQuotState is BuySellQuotFailed) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 24, + children: [ + Icon(Icons.warning_amber_outlined, size: 48), + Column( + spacing: 10, + children: [ + Text( + S.of(context).could_not_load_quotes, + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500), + ), + Text((widget.buySellViewModel.buySellQuotState as BuySellQuotFailed).errorMessage ?? + S.of(context).please_try_again_later) + ], + ) + ], + ); + } + + if(widget.buySellViewModel.buySellQuotState is BuySellQuotLoading) { + return Center(child: Row(mainAxisAlignment:MainAxisAlignment.center, spacing:8, children: [ + CupertinoActivityIndicator(), + Text(S.of(context).loading_rates, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),) + ],),); + } + + return Padding( + padding: EdgeInsets.symmetric(horizontal: 18), + child: NewListSections(showHeader: true, sections: { + "": [ + ListItemRegularRow( + keyValue: "payment method", + label: S.of(context).payment_method, + showArrow: true, + onTap: (){ + final page =BuySellPaymentMethodPage(buySellViewModel: widget.buySellViewModel); + Navigator.of(context).push(CupertinoPageRoute(builder: (context)=>Material(color: Colors.transparent,child: page,))); + }, + trailingText: widget.buySellViewModel.selectedPaymentMethod?.title) + ], + S.of(context).available_providers: [ + ...widget.buySellViewModel.sortedRecommendedQuotes.map(quoteListItem), + ListItemDropdown( + keyValue: "more options", + label: S.of(context).more_options, + onTap: () { + setState(() { + _allProvidersExpanded = !_allProvidersExpanded; + }); + }), + if (_allProvidersExpanded) + ...widget.buySellViewModel.sortedQuotes.map(quoteListItem) + ] + }), + ); + }, )) ], )), ); } - String get _pageTitle => buySellViewModel.mode == BuySellPageMode.buy + String get _pageTitle => (widget.buySellViewModel.mode == BuySellPageMode.buy ? S.current.buy - : S.current.sell + " " + (buySellViewModel.cryptoCurrency.fullName ?? ""); + : S.current.sell) + " " + (widget.buySellViewModel.cryptoCurrency.fullName ?? ""); + + ListItem quoteListItem(Quote quote) => ListItemRegularRow( + keyValue: quote.provider.title, + label: quote.rampName ?? quote.provider.title, + secondaryLabel: quote.rampName != null? quote.provider.title : null, + subtitle: quote.badges.isEmpty ? null : quote.badges.join(" - "), + subtitleColor: Theme.of(context).colorScheme.primary, + iconPath: quote.darkIconPath, + onTap: (){ + widget.buySellViewModel.changeOption(quote); + navigateToConfirmation(context); + }, + leadingIconSize: 32, + trailingWidget: Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.center, + spacing: 4, + children: [ + Text(widget.buySellViewModel.amountForQuote(quote).toStringWithSymbol(fractionalDigits: 8)), + Text("= ${widget.buySellViewModel.fiatAmountForQuote(quote).toStringWithSymbol(fractionalDigits: 2, trimZeros: false)}", style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant)) + + ],)); + + void navigateToConfirmation(BuildContext context) { + + final page = BuySellConfirmationPage(buySellViewModel: widget.buySellViewModel); + Navigator.of(context).push(CupertinoPageRoute(builder: (context)=>Material(color: Colors.transparent,child: page,))); + } } diff --git a/lib/new-ui/pages/buy_sell/buy_sell_redirecting_page.dart b/lib/new-ui/pages/buy_sell/buy_sell_redirecting_page.dart new file mode 100644 index 0000000000..f96531b82c --- /dev/null +++ b/lib/new-ui/pages/buy_sell/buy_sell_redirecting_page.dart @@ -0,0 +1,125 @@ +import 'package:cake_wallet/buy/sell_buy_states.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; +import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class BuySellRedirectingPage extends StatefulWidget { + const BuySellRedirectingPage({super.key, required this.buySellViewModel}); + + final BuySellViewModel buySellViewModel; + + @override + State createState() => _BuySellRedirectingPageState(); +} + +class _BuySellRedirectingPageState extends State { + bool _hasRedirected = false; + + @override + void initState() { + super.initState(); + Future.delayed(Duration(seconds: 2)).then((_) async { + WidgetsBinding.instance + .addPostFrameCallback((_) => widget.buySellViewModel.launchTrade(context)); + setState(() { + _hasRedirected = true; + }); + }); + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: false, + child: 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: Observer( + builder: (_) { + final showExitButton = + _hasRedirected || widget.buySellViewModel.buySellQuotState is BuySellQuotFailed; + + return SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox.shrink(), + Observer( + builder: (_) { + if (widget.buySellViewModel.buySellQuotState is BuySellQuotFailed) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 24, + children: [ + Icon(Icons.warning_amber_outlined, size: 48), + Column( + spacing: 10, + children: [ + Text( + S.of(context).could_not_proceed_with_trade, + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500), + ), + Text((widget.buySellViewModel.buySellQuotState as BuySellQuotFailed) + .errorMessage ?? + S.of(context).please_try_again_later) + ], + ) + ], + ); + } + return Column( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 24, + children: [ + CakeImageWidget( + imageUrl: widget.buySellViewModel.selectedQuote!.darkIconPath, + width: 64, + height: 64, + ), + Column( + spacing: 10, + children: [ + Text( + "${S.of(context).connecting_you_to} ${widget.buySellViewModel.selectedQuote!.provider.title}...", + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500), + ), + Text( + "${widget.buySellViewModel.fiatAmount} ${widget.buySellViewModel.fiatCurrency} → ${widget.buySellViewModel.amountForQuote(widget.buySellViewModel.selectedQuote!).toStringWithSymbol(fractionalDigits: 8)}") + ], + ) + ], + ); + }, + ), + showExitButton + ? Padding( + padding: EdgeInsets.all(18), + child: NewPrimaryButton( + onPressed: Navigator.of(context).pop, + text: S.of(context).close, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + ), + ) + : SizedBox.shrink() + ], + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart b/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart index 7e6efd0044..6d5343ccc8 100644 --- a/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart +++ b/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart @@ -46,7 +46,15 @@ class BuySellSelectorModal extends StatelessWidget { void openBuySellPage(BuildContext context, BuySellPageMode mode) { Navigator.of(context).pop(); - showModalBottomSheet(useSafeArea:true, isScrollControlled: true, context: context, builder: (modalContext)=>ModalNavigator(rootPage: getIt.get(param1: mode), parentContext: context,)); + final page = getIt.get(param1: mode); + showModalBottomSheet( + useSafeArea: true, + isScrollControlled: true, + context: context, + builder: (modalContext) => ModalNavigator( + rootPage: page, + parentContext: context, + )); } } diff --git a/lib/new-ui/widgets/receive_page/receive_top_bar.dart b/lib/new-ui/widgets/receive_page/receive_top_bar.dart index b41eb9c209..d345156dd0 100644 --- a/lib/new-ui/widgets/receive_page/receive_top_bar.dart +++ b/lib/new-ui/widgets/receive_page/receive_top_bar.dart @@ -12,6 +12,7 @@ class ModalTopBar extends StatelessWidget { this.onTrailingPressed=nothing, this.leadingIcon, this.trailingIcon, + this.bottomText, this.padding, this.leadingWidget, this.trailingWidget}) { @@ -25,6 +26,7 @@ class ModalTopBar extends StatelessWidget { final String title; final String? subtitle; + final String? bottomText; final VoidCallback onLeadingPressed; final VoidCallback onTrailingPressed; final Widget? leadingIcon; @@ -37,25 +39,32 @@ class ModalTopBar extends StatelessWidget { @override Widget build(BuildContext context) { + final hasBottomText =bottomText != null && bottomText!.isNotEmpty; return Padding( padding: padding??EdgeInsets.all(18), child: Stack( alignment: Alignment.topCenter, children: [ Positioned( - top: 6, + top: hasBottomText ? -4 : 6, child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, spacing: 4, children: [ - AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: Text( - title, - key: ValueKey(title), - style: Theme.of(context).textTheme.headlineMedium, - ), + Column( + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: Text( + title, + key: ValueKey(title), + style: TextStyle(fontSize: hasBottomText ? 16 : 18, fontWeight: FontWeight.w600), + ), + ), + if(hasBottomText) + Text(bottomText!, style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),) + ], ), if (subtitle != null && subtitle!.isNotEmpty) Text( diff --git a/lib/src/widgets/cake_image_widget.dart b/lib/src/widgets/cake_image_widget.dart index f714bcd0c8..5b865324c0 100644 --- a/lib/src/widgets/cake_image_widget.dart +++ b/lib/src/widgets/cake_image_widget.dart @@ -1,3 +1,4 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:vector_graphics/vector_graphics.dart'; @@ -86,7 +87,7 @@ class CakeImageWidget extends StatelessWidget { allowDrawingOutsideViewBox: allowDrawingOutsideViewBox ?? false, fit: fit ?? BoxFit.contain, placeholderBuilder: (_) { - return loadingWidget ?? const Center(child: CircularProgressIndicator()); + return loadingWidget ?? SizedBox(height: height, width: width, child: Center(child: CupertinoActivityIndicator())); }, errorBuilder: (_, __, ___) => _buildErrorWidget(context), ) @@ -99,7 +100,7 @@ class CakeImageWidget extends StatelessWidget { filterQuality: filterQuality ?? FilterQuality.medium, loadingBuilder: (_, Widget child, ImageChunkEvent? progress) { if (progress == null) return child; - return loadingWidget ?? const Center(child: CircularProgressIndicator()); + return loadingWidget ?? SizedBox(height: height, width: width, child: Center(child: CupertinoActivityIndicator())); }, errorBuilder: (_, __, ___) => _buildErrorWidget(context), ); diff --git a/lib/src/widgets/new_list_row/list_item_dropdown_widget.dart b/lib/src/widgets/new_list_row/list_item_dropdown_widget.dart index 011f90ea9e..1a0ef17def 100644 --- a/lib/src/widgets/new_list_row/list_item_dropdown_widget.dart +++ b/lib/src/widgets/new_list_row/list_item_dropdown_widget.dart @@ -29,31 +29,29 @@ class ListItemDropdownWidget extends StatelessWidget { return ListItemStyleWrapper( isFirstInSection: isFirstInSection, isLastInSection: isLastInSection, + onTap: onTap, builder: (context, textStyle, labelStyle) { - return InkWell( - onTap: onTap, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(label, style: textStyle), - Row( - children: [ - if (trailingText != null) - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text( - trailingText!, - style: labelStyle, - ), + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: textStyle), + Row( + children: [ + if (trailingText != null) + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text( + trailingText!, + style: labelStyle, ), - Icon( - Icons.keyboard_arrow_down, - color: Theme.of(context).colorScheme.onSurfaceVariant, ), - ], - ), - ], - ), + Icon( + Icons.keyboard_arrow_down, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ], + ), + ], ); }, ); diff --git a/lib/src/widgets/new_list_row/list_item_regular_row_widget.dart b/lib/src/widgets/new_list_row/list_item_regular_row_widget.dart index b2301b8814..dc552fd668 100644 --- a/lib/src/widgets/new_list_row/list_item_regular_row_widget.dart +++ b/lib/src/widgets/new_list_row/list_item_regular_row_widget.dart @@ -23,22 +23,25 @@ class ListItemRegularRowWidget extends StatelessWidget { this.foregroundColor, this.trailingIconSize, this.bottomWidget, + this.subtitleColor, this.trailingWidget, this.copyableText, this.leadingIconErrorWidget, this.leadingIconSize, this.badgeIconSize, - this.iconColor}); + this.iconColor, this.secondaryLabel}); final String keyValue; final String label; final String? subtitle; final String? trailingText; + final String? secondaryLabel; final String? iconPath; final String? badgeIconPath; final VoidCallback? onTap; final bool isFirstInSection; final bool isLastInSection; + final Color? subtitleColor; final bool showArrow; final String? trailingIconPath; final Widget? bottomWidget; @@ -131,14 +134,21 @@ class ListItemRegularRowWidget extends StatelessWidget { color: Theme.of(context).colorScheme.primary), ) else - Text(label, - style: foregroundColor == null - ? textStyle - : textStyle.copyWith(color: foregroundColor)), + Row( + spacing: 4, + children: [ + Text(label, + style: foregroundColor == null + ? textStyle + : textStyle.copyWith(color: foregroundColor)), + if(secondaryLabel != null) + Text(secondaryLabel!, style: textStyle.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),) + ], + ), if (subtitle != null) Text( subtitle!, - style: labelStyle.copyWith(fontSize: 12), + style: subtitleColor == null ? labelStyle.copyWith(fontSize: 12) : labelStyle.copyWith(fontSize: 12, color: subtitleColor), ), ], ), diff --git a/lib/src/widgets/new_list_row/new_list_section.dart b/lib/src/widgets/new_list_row/new_list_section.dart index 8abef379da..e5097e0a7a 100644 --- a/lib/src/widgets/new_list_row/new_list_section.dart +++ b/lib/src/widgets/new_list_row/new_list_section.dart @@ -98,6 +98,8 @@ class NewListSections extends StatelessWidget { iconPath: item.iconPath, badgeIconPath: item.badgeIconPath, trailingIconPath: item.trailingIconPath, + secondaryLabel: item.secondaryLabel, + subtitleColor: item.subtitleColor, onTap: tapHandlers[item.keyValue] ?? item.onTap, isFirstInSection: isFirst, isLastInSection: isLast, diff --git a/lib/view_model/buy/buy_sell_view_model.dart b/lib/view_model/buy/buy_sell_view_model.dart index 100f13329e..25e8ef8628 100644 --- a/lib/view_model/buy/buy_sell_view_model.dart +++ b/lib/view_model/buy/buy_sell_view_model.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'package:cake_wallet/buy/buy_provider.dart'; import 'package:cake_wallet/buy/buy_quote.dart'; @@ -13,8 +14,9 @@ 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:cake_wallet/store/dashboard/fiat_conversion_store.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -28,7 +30,7 @@ class BuySellViewModel = BuySellViewModelBase with _$BuySellViewModel; abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with Store { BuySellViewModelBase( AppStore appStore, - {required this.mode} + {required this.mode, required this.fiatConversionStore} ) : _cryptoAmount = '', fiatAmount = '', cryptoCurrencyAddress = '', @@ -60,6 +62,8 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S late Timer bestRateSync; + final FiatConversionStore fiatConversionStore; + List get availableBuyProviders { final providerTypes = ProvidersHelper.getAvailableBuyProviderTypes(); return providerTypes @@ -153,7 +157,28 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S @observable bool skipIsReadyToTradeReaction = false; + @computed + String? get maxFiatAmount { + if ((sortedQuotes.isEmpty && sortedRecommendedQuotes.isEmpty) || + buySellQuotState is! BuySellQuotLoaded) { + return null; + } + + final allQuotes = sortedRecommendedQuotes.followedBy(sortedQuotes); + + final maxAmount = allQuotes.fold(0.0, (current, item) { + final limitMax = item.limits?.max?.toDouble() ?? 0.0; + return max(current, limitMax); + }); + return maxAmount.toStringAsFixed(2); + } + + Money amountForQuote(Quote quote) => Money.parse(double.parse(fiatAmount)/quote.rate, cryptoCurrency); + + Money fiatAmountForQuote(Quote quote) { + return Money.parse((fiatConversionStore.prices[cryptoCurrency]! * double.parse(amountForQuote(quote).toString())).toStringAsFixed(2), fiatCurrency); + } // based on usd values, should have roughly equal worth (was done with ai though so it's subject to correction) static final Map> _defaultAmountsMap = { @@ -252,6 +277,13 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency); } + + @computed + Iterable get activeWalletCurrencies => wallet.balance.keys; + + @computed + bool get hasMultipleCurrencies => activeWalletCurrencies.length > 1; + @action void changeCryptoCurrencyAddress(String address) => cryptoCurrencyAddress = address; @@ -401,6 +433,9 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S cryptoCurrencyAddress = _getInitialCryptoCurrencyAddress(); paymentMethodState = InitialPaymentMethod(); buySellQuotState = InitialBuySellQuotState(); + sortedRecommendedQuotes.clear(); + sortedQuotes.clear(); + paymentMethods.clear(); await _getAvailablePaymentTypes(); await calculateBestRate(); } @@ -458,16 +493,25 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S final List validProviders = providerList.where((provider) { if (mode == BuySellPageMode.buy) { - return provider.supportedCryptoList - .any((pair) => pair.from == cryptoCurrency && pair.to == fiatCurrency); + return provider.supportedCryptoList.any((pair) => + pair.from.symbol == cryptoCurrency.symbol && + pair.from.tag == cryptoCurrency.tag && + pair.to.symbol == fiatCurrency.symbol && + pair.to.tag == fiatCurrency.tag + ); } else { - return provider.supportedFiatList - .any((pair) => pair.from == fiatCurrency && pair.to == cryptoCurrency); + return provider.supportedFiatList.any((pair) => + pair.from.symbol == fiatCurrency.symbol && + pair.from.tag == fiatCurrency.tag && + pair.to.symbol == cryptoCurrency.symbol && + pair.to.tag == cryptoCurrency.tag + ); } }).toList(); + if (validProviders.isEmpty) { - buySellQuotState = BuySellQuotFailed(); + buySellQuotState = BuySellQuotFailed(errorMessage: "Couldn't find a provider that supports ${cryptoCurrency.fullName}."); return; } diff --git a/pubspec_base.yaml b/pubspec_base.yaml index c5525007bc..c46043726c 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -295,6 +295,7 @@ flutter: - assets/new-ui/crypto_full_icons/ - assets/new-ui/hardware_wallets/ - assets/new-ui/node_speed_badges/ + - assets/new-ui/buy_payment_methods/ fonts: - family: Lato diff --git a/res/pictures/buy_payment_methods/all_methods.svg b/res/pictures/buy_payment_methods/all_methods.svg new file mode 100644 index 0000000000..b3e2adade1 --- /dev/null +++ b/res/pictures/buy_payment_methods/all_methods.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/pictures/buy_payment_methods/apple_pay.svg b/res/pictures/buy_payment_methods/apple_pay.svg new file mode 100644 index 0000000000..dd7f18b622 --- /dev/null +++ b/res/pictures/buy_payment_methods/apple_pay.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/res/pictures/buy_payment_methods/bank_transfer.svg b/res/pictures/buy_payment_methods/bank_transfer.svg new file mode 100644 index 0000000000..2eccd60ca6 --- /dev/null +++ b/res/pictures/buy_payment_methods/bank_transfer.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/pictures/buy_payment_methods/debit_card.svg b/res/pictures/buy_payment_methods/debit_card.svg new file mode 100644 index 0000000000..4b68f70c63 --- /dev/null +++ b/res/pictures/buy_payment_methods/debit_card.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/pictures/buy_payment_methods/google_pay.svg b/res/pictures/buy_payment_methods/google_pay.svg new file mode 100644 index 0000000000..836c8acb18 --- /dev/null +++ b/res/pictures/buy_payment_methods/google_pay.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/res/pictures/buy_payment_methods/paypal.svg b/res/pictures/buy_payment_methods/paypal.svg new file mode 100644 index 0000000000..d723ede3e3 --- /dev/null +++ b/res/pictures/buy_payment_methods/paypal.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 207ee288cf..9929af1182 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -93,6 +93,7 @@ "available": "Available", "available_balance": "Available Balance", "available_balance_description": "The “Available Balance” or “Confirmed Balance” are funds that can be spent immediately. If funds appear in the lower balance but not the top balance, then you must wait a few minutes for the incoming funds to get more network confirmations. After they get more confirmations, they will be spendable.", + "available_providers": "Available Providers", "avg_savings": "Avg. Savings", "awaitDAppProcessing": "Kindly wait for the dApp to finish processing.", "awaiting_payment_confirmation": "Awaiting Payment Confirmation", @@ -254,6 +255,7 @@ "connect_your_hardware_wallet_usb": "Connect your hardware wallet using USB", "connected": "Connected", "connection_sync": "Connection and sync", + "connecting_you_to": "Connecting you to", "connections": "Connections", "connections_desc": "Manage connections to services and third party APIs.", "connectWalletPrompt": "Connect your wallet with WalletConnect to make transactions", @@ -279,6 +281,8 @@ "copy_payjoin_url": "Copy Payjoin URL", "copyWalletConnectLink": "Copy the WalletConnect link from dApp and paste here", "corrupted_seed_notice": "The files for this wallet are corrupted and are unable to be opened. Please view the seed phrase, save it, and restore the wallet.\n\nIf the value is empty, then the seed was unable to be correctly recovered.", + "could_not_load_quotes": "Could not load quotes", + "could_not_proceed_with_trade": "Could not proceed with trade", "countries": "Countries", "create_account": "Create Account", "create_backup": "Create backup", @@ -480,6 +484,7 @@ "extra_id": "Extra ID:", "extracted_address_content": "You will be sending funds to\n${recipient_name}", "failed_authentication": "Failed authentication. ${state_error}", + "failed_to_load_payment_methods": "Failed to load payment methods", "faq": "FAQ", "favorite_token": "Favorite token", "favorite_token_desc": "The favorite token's balance will show on the balance card.", @@ -538,6 +543,7 @@ "hide_address": "Hide address", "hide_details": "Hide Details", "high_contrast_theme": "High Contrast Theme", + "highest_success_rate": "Highest success rate", "history": "History", "home": "Home", "home_screen_settings": "Home screen settings", @@ -614,6 +620,7 @@ "live_fee_rates": "Live fee rates via API", "load_more": "Load more", "loading": "Loading", + "loading_rates": "Loading rates...", "loading_your_wallet": "Loading your wallet", "login": "Login", "logout": "Logout", @@ -765,6 +772,7 @@ "payment_id": "Payment ID: ", "payment_invoices": "Payment Invoices", "payment_made_easy": "Payments made easy", + "payment_method": "Payment Method", "payment_was_received": "Your payment was received.", "payments": "Payments", "pending": " (pending)", @@ -796,6 +804,7 @@ "please_reference_document": "Please reference the documents below for more information.", "please_select": "Please select:", "please_select_backup_file": "Please select backup file and enter backup password.", + "please_try_again_later": "Please try again later.", "please_try_to_connect_to_another_node": "Please try to connect to another node", "please_wait": "Please wait", "polygonscan_history": "PolygonScan history", @@ -818,6 +827,7 @@ "private_key": "Private key", "private_memo_optional": "Private Memo (optional)", "proceed_after_one_minute": "If the screen doesn’t proceed after 1 minute, check your email.", + "proceed": "Proceed", "proceed_on_device": "Proceed on your device", "proceed_on_device_description": "Please follow the instructions prompted on your hardware wallet", "processing": "Processing", @@ -837,6 +847,7 @@ "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", "quantity": "Quantity", "question_to_disable_2fa": "Are you sure that you want to disable Cake 2FA? A 2FA code will no longer be needed to access the wallet and certain functions.", + "rate": "Rate", "receivable_balance": "Receivable Balance", "receive": "Receive", "receive_amount": "Amount", @@ -1314,6 +1325,7 @@ "unsupported_asset": "We don't support this action for this asset. Please create or switch to a wallet of a supported asset type.", "uptime": "Uptime", "upto": "up to ${value}", + "up_to": "Up to", "usb": "USB", "use": "Switch to ", "use_blink_protection": "Use Blink Protection", From 1f4054d63245058b2cfa18e0464cff10cb0edf7b Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Fri, 3 Jul 2026 14:09:52 +0200 Subject: [PATCH 06/11] reformat --- .../pages/buy_sell/buy_sell_amount_page.dart | 768 +++++++++--------- .../buy_sell/buy_sell_provider_page.dart | 119 +-- .../buy_sell/buy_sell_selector_modal.dart | 123 ++- .../widgets/coins_page/cards/cards_view.dart | 3 +- lib/new-ui/widgets/floating_amount_input.dart | 37 +- lib/utils/exception_handler.dart | 2 +- lib/view_model/buy/buy_sell_view_model.dart | 9 +- 7 files changed, 567 insertions(+), 494 deletions(-) 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 ec22421b1d..772cfc77a6 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,435 +1,435 @@ -import 'dart:async'; + import 'dart:async'; -import 'package:cake_wallet/buy/payment_method.dart'; -import 'package:cake_wallet/buy/sell_buy_states.dart'; -import 'package:cake_wallet/entities/fiat_currency.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/new-ui/pages/buy_sell/buy_sell_provider_page.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/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/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; -import 'package:cake_wallet/themes/core/theme_extension.dart'; -import 'package:cake_wallet/utils/show_pop_up.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/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:mobx/mobx.dart'; + import 'package:cake_wallet/buy/payment_method.dart'; + import 'package:cake_wallet/buy/sell_buy_states.dart'; + import 'package:cake_wallet/entities/fiat_currency.dart'; + import 'package:cake_wallet/generated/i18n.dart'; + import 'package:cake_wallet/new-ui/pages/buy_sell/buy_sell_provider_page.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/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/src/widgets/alert_with_one_action.dart'; + import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; + import 'package:cake_wallet/themes/core/theme_extension.dart'; + import 'package:cake_wallet/utils/show_pop_up.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/cupertino.dart'; + import 'package:flutter/material.dart'; + import 'package:flutter_mobx/flutter_mobx.dart'; + import 'package:mobx/mobx.dart'; -class NewBuySellAmountPage extends StatefulWidget { - const NewBuySellAmountPage({super.key, required this.buySellViewModel}); + class NewBuySellAmountPage extends StatefulWidget { + const NewBuySellAmountPage({super.key, required this.buySellViewModel}); - final BuySellViewModel buySellViewModel; + final BuySellViewModel buySellViewModel; - @override - State createState() => _NewBuySellAmountPageState(); -} + @override + State createState() => _NewBuySellAmountPageState(); + } -class _NewBuySellAmountPageState extends State { - bool _customAmountMode = false; - bool _isLoadingPaymentMethods = false; - final customInputController = TextEditingController(); - final customInputFocusNode = FocusNode(); + class _NewBuySellAmountPageState extends State { + bool _customAmountMode = false; + bool _isLoadingPaymentMethods = false; + final customInputController = TextEditingController(); + final customInputFocusNode = FocusNode(); - @override - void initState() { - super.initState(); + @override + void initState() { + super.initState(); - // this is a hack for the "up to" display to show the actual upper limit. - // limits are loaded alongside quotes, and quotes for some providers only load properly with an amount and payment method selected. - // it'll be refactored soon anyway, i guess. - widget.buySellViewModel.changeFiatAmount(amount: "1000"); - widget.buySellViewModel.calculateBestRate(); - when( - (_) => widget.buySellViewModel.paymentMethodState is PaymentMethodLoaded, - () => widget.buySellViewModel.selectedPaymentMethod = widget.buySellViewModel.paymentMethods - .firstWhere((item) => item.paymentMethodType == PaymentType.all)); - } + // this is a hack for the "up to" display to show the actual upper limit. + // limits are loaded alongside quotes, and quotes for some providers only load properly with an amount and payment method selected. + // it'll be refactored soon anyway, i guess. + widget.buySellViewModel.changeFiatAmount(amount: "1000"); + widget.buySellViewModel.calculateBestRate(); + when( + (_) => widget.buySellViewModel.paymentMethodState is PaymentMethodLoaded, + () => widget.buySellViewModel.selectedPaymentMethod = widget.buySellViewModel.paymentMethods + .firstWhere((item) => item.paymentMethodType == PaymentType.all)); + } - @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, + @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: [ - Observer( - builder: (_) => ModalTopBar( - title: _pageTitle, - bottomText: !_customAmountMode || widget.buySellViewModel.maxFiatAmount == null - ? null - : "${S.of(context).up_to} ~${widget.buySellViewModel.maxFiatAmount} ${widget.buySellViewModel.fiatCurrency.title}", - leadingIcon: Icon(Icons.close), - onLeadingPressed: Navigator.of(context, rootNavigator: true).pop, + child: SafeArea( + child: Column( + children: [ + Observer( + builder: (_) => ModalTopBar( + title: _pageTitle, + bottomText: !_customAmountMode || widget.buySellViewModel.maxFiatAmount == null + ? null + : "${S.of(context).up_to} ~${widget.buySellViewModel.maxFiatAmount} ${widget.buySellViewModel.fiatCurrency.title}", + leadingIcon: Icon(Icons.close), + onLeadingPressed: Navigator.of(context, rootNavigator: true).pop, + ), ), - ), - Expanded( - child: Observer( - builder: (_) => AnimatedSwitcher( - duration: Duration(milliseconds: 300), - child: _customAmountMode - ? BuySellCustomAmountInput( - fiatCurrency: widget.buySellViewModel.fiatCurrency, - cryptoCurrency: widget.buySellViewModel.cryptoCurrency, - cryptoAmount: widget.buySellViewModel.cryptoAmount, - isLoading: _isLoadingPaymentMethods, - hasCurrencySelector: widget.buySellViewModel.hasMultipleCurrencies, - onCurrencySelectorPressed: () => selectCryptoCurrency(context), - controller: customInputController, - focusNode: customInputFocusNode, - onContinuePressed: () { - navigateToProviders(context); - }, - onChanged: (amount) => - widget.buySellViewModel.changeFiatAmount(amount: amount), - ) - : BuySellDefaultAmountSelector( - key: ValueKey(0), - defaultAmounts: widget.buySellViewModel.defaultAmounts, - fiatCurrency: widget.buySellViewModel.fiatCurrency, - currentAmount: widget.buySellViewModel.fiatAmount, - hasCurrencySelector: widget.buySellViewModel.hasMultipleCurrencies, - onCurrencySelectorPressed: () => selectCryptoCurrency(context), - cryptoCurrency: widget.buySellViewModel.cryptoCurrency, - isLoading: _isLoadingPaymentMethods, - mode: widget.buySellViewModel.mode, - onSelected: (amount) async { - if (amount == null) { - // this resets the rate and prevents showing 0 usd = 0.something btc - await widget.buySellViewModel.changeFiatAmount(amount: ""); - setState(() { - _customAmountMode = true; - }); - customInputFocusNode.requestFocus(); - } else { - await widget.buySellViewModel.changeFiatAmount(amount: amount); + Expanded( + child: Observer( + builder: (_) => AnimatedSwitcher( + duration: Duration(milliseconds: 300), + child: _customAmountMode + ? BuySellCustomAmountInput( + fiatCurrency: widget.buySellViewModel.fiatCurrency, + cryptoCurrency: widget.buySellViewModel.cryptoCurrency, + cryptoAmount: widget.buySellViewModel.cryptoAmount, + isLoading: _isLoadingPaymentMethods, + hasCurrencySelector: widget.buySellViewModel.hasMultipleCurrencies, + onCurrencySelectorPressed: () => selectCryptoCurrency(context), + controller: customInputController, + focusNode: customInputFocusNode, + onContinuePressed: () { navigateToProviders(context); - } - }, - ), - ), - )) - ], + }, + onChanged: (amount) => + widget.buySellViewModel.changeFiatAmount(amount: amount), + ) + : BuySellDefaultAmountSelector( + key: ValueKey(0), + defaultAmounts: widget.buySellViewModel.defaultAmounts, + fiatCurrency: widget.buySellViewModel.fiatCurrency, + currentAmount: widget.buySellViewModel.fiatAmount, + hasCurrencySelector: widget.buySellViewModel.hasMultipleCurrencies, + onCurrencySelectorPressed: () => selectCryptoCurrency(context), + cryptoCurrency: widget.buySellViewModel.cryptoCurrency, + isLoading: _isLoadingPaymentMethods, + mode: widget.buySellViewModel.mode, + onSelected: (amount) async { + if (amount == null) { + // this resets the rate and prevents showing 0 usd = 0.something btc + await widget.buySellViewModel.changeFiatAmount(amount: ""); + setState(() { + _customAmountMode = true; + }); + customInputFocusNode.requestFocus(); + } else { + await widget.buySellViewModel.changeFiatAmount(amount: amount); + navigateToProviders(context); + } + }, + ), + ), + )) + ], + ), ), - ), - ); - } - - void selectCryptoCurrency(BuildContext context) => CurrencyPickerSheet.show( - context: context, - args: CurrencyPickerArgs( - items: widget.buySellViewModel.activeWalletCurrencies.toList(), - onSelected: (item) => widget.buySellViewModel.changeCryptoCurrency(currency: item), - symbolResolver: widget.buySellViewModel.amountParsingProxy.getCryptoSymbol, - )); - - String get _pageTitle => widget.buySellViewModel.mode == BuySellPageMode.buy - ? S.current.buy - : S.current.sell + - ((widget.buySellViewModel.cryptoCurrencies.length == 1) - ? " ${widget.buySellViewModel.cryptoCurrencies.first.fullName}" - : ""); - - Future navigateToProviders(BuildContext context) async { - if (_isLoadingPaymentMethods) { - return; + ); } - try { - setState(() { - _isLoadingPaymentMethods = true; - }); + void selectCryptoCurrency(BuildContext context) => CurrencyPickerSheet.show( + context: context, + args: CurrencyPickerArgs( + items: widget.buySellViewModel.activeWalletCurrencies.toList(), + onSelected: (item) => widget.buySellViewModel.changeCryptoCurrency(currency: item), + symbolResolver: widget.buySellViewModel.amountParsingProxy.getCryptoSymbol, + )); - await asyncWhen((_) => [PaymentMethodLoaded, PaymentMethodFailed] - .contains(widget.buySellViewModel.paymentMethodState.runtimeType)); + String get _pageTitle => widget.buySellViewModel.mode == BuySellPageMode.buy + ? S.current.buy + : S.current.sell + + ((widget.buySellViewModel.cryptoCurrencies.length == 1) + ? " ${widget.buySellViewModel.cryptoCurrencies.first.fullName}" + : ""); - if (widget.buySellViewModel.paymentMethodState is PaymentMethodFailed) { - showPopUp( - context: context, - builder: (context) => AlertWithOneAction( - alertTitle: S.of(context).failed_to_load_payment_methods, - alertContent: S.of(context).please_try_again_later, - buttonText: "OK", - buttonAction: Navigator.of(context).pop)); + Future navigateToProviders(BuildContext context) async { + if (_isLoadingPaymentMethods) { return; } - widget.buySellViewModel.selectedPaymentMethod = widget.buySellViewModel.paymentMethods - .firstWhere((item) => item.paymentMethodType == PaymentType.all); + try { + setState(() { + _isLoadingPaymentMethods = true; + }); + + await asyncWhen((_) => [PaymentMethodLoaded, PaymentMethodFailed] + .contains(widget.buySellViewModel.paymentMethodState.runtimeType)); + + if (widget.buySellViewModel.paymentMethodState is PaymentMethodFailed) { + showPopUp( + context: context, + builder: (context) => AlertWithOneAction( + alertTitle: S.of(context).failed_to_load_payment_methods, + alertContent: S.of(context).please_try_again_later, + buttonText: "OK", + buttonAction: Navigator.of(context).pop)); + return; + } + + widget.buySellViewModel.selectedPaymentMethod = widget.buySellViewModel.paymentMethods + .firstWhere((item) => item.paymentMethodType == PaymentType.all); - // unawaited because BuySellProviderPage has a nice little "loading rates..." ui thingie - unawaited(widget.buySellViewModel.calculateBestRate()); + // unawaited because BuySellProviderPage has a nice little "loading rates..." ui thingie + unawaited(widget.buySellViewModel.calculateBestRate()); - final page = BuySellProviderPage(buySellViewModel: widget.buySellViewModel); - Navigator.of(context).push(CupertinoPageRoute( - builder: (context) => Material(color: Colors.transparent, child: page))); - } finally { - setState(() { - _isLoadingPaymentMethods = false; - }); + final page = BuySellProviderPage(buySellViewModel: widget.buySellViewModel); + Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => Material(color: Colors.transparent, child: page))); + } finally { + setState(() { + _isLoadingPaymentMethods = false; + }); + } } } -} -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.isLoading, - required this.onChanged, - required this.focusNode, - required this.hasCurrencySelector, - required this.onCurrencySelectorPressed}); + 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.isLoading, + required this.onChanged, + required this.focusNode, + required this.hasCurrencySelector, + required this.onCurrencySelectorPressed}); - final FiatCurrency fiatCurrency; - final CryptoCurrency cryptoCurrency; - final String cryptoAmount; - final TextEditingController controller; - final FocusNode focusNode; - final bool isLoading; - final bool hasCurrencySelector; - final VoidCallback onCurrencySelectorPressed; - final VoidCallback onContinuePressed; - final Function(String) onChanged; + final FiatCurrency fiatCurrency; + final CryptoCurrency cryptoCurrency; + final String cryptoAmount; + final TextEditingController controller; + final FocusNode focusNode; + final bool isLoading; + final bool hasCurrencySelector; + final VoidCallback onCurrencySelectorPressed; + final VoidCallback onContinuePressed; + final Function(String) onChanged; - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox.shrink(), - Column( - spacing: 8, - children: [ - BuySellCurrencyPickerPill(curr: cryptoCurrency, onTap: onCurrencySelectorPressed), - SizedBox.shrink(), - FloatingAmountInput( - currency: fiatCurrency, - focusNode: focusNode, - 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), + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox.shrink(), + Column( + spacing: 8, + children: [ + BuySellCurrencyPickerPill(curr: cryptoCurrency, onTap: onCurrencySelectorPressed), + SizedBox.shrink(), + FloatingAmountInput( + currency: fiatCurrency, + focusNode: focusNode, + controller: controller, + onChanged: onChanged, ), - ) - ], - ), - Padding( - padding: const EdgeInsets.all(18.0), - child: NewPrimaryButton( - onPressed: onContinuePressed, - isLoading: isLoading, - text: S.of(context).continue_text, - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary), - ) - ], - ); + 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, + isLoading: isLoading, + 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, - required this.defaultAmounts, - required this.fiatCurrency, - required this.mode, - required this.onSelected, - this.currentAmount, - required this.isLoading, - required this.hasCurrencySelector, - required this.onCurrencySelectorPressed, - required this.cryptoCurrency}); + class BuySellDefaultAmountSelector extends StatelessWidget { + const BuySellDefaultAmountSelector( + {super.key, + required this.defaultAmounts, + required this.fiatCurrency, + required this.mode, + required this.onSelected, + this.currentAmount, + required this.isLoading, + required this.hasCurrencySelector, + required this.onCurrencySelectorPressed, + required this.cryptoCurrency}); - final List defaultAmounts; - final String? currentAmount; - final bool isLoading; - final bool hasCurrencySelector; - final VoidCallback onCurrencySelectorPressed; - final CryptoCurrency cryptoCurrency; - final FiatCurrency fiatCurrency; - final BuySellPageMode mode; - final Function(String?) onSelected; + final List defaultAmounts; + final String? currentAmount; + final bool isLoading; + final bool hasCurrencySelector; + final VoidCallback onCurrencySelectorPressed; + final CryptoCurrency cryptoCurrency; + final FiatCurrency fiatCurrency; + final BuySellPageMode mode; + final Function(String?) onSelected; - @override - Widget build(BuildContext context) { - return Column( - spacing: 24, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - BuySellCurrencyPickerPill(curr: cryptoCurrency, onTap: onCurrencySelectorPressed), - Text( - mode == BuySellPageMode.sell - ? S.of(context).choose_amount_to_sell - : S.of(context).choose_amount_to_buy, - style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 18.0), - child: GridView.builder( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - // +1 for "custom" option - itemCount: defaultAmounts.length + 1, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, crossAxisSpacing: 8, mainAxisSpacing: 16, mainAxisExtent: 105), - itemBuilder: (context, index) { - final String? item = index == defaultAmounts.length ? null : defaultAmounts[index]; + @override + Widget build(BuildContext context) { + return Column( + spacing: 24, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + BuySellCurrencyPickerPill(curr: cryptoCurrency, onTap: onCurrencySelectorPressed), + Text( + mode == BuySellPageMode.sell + ? S.of(context).choose_amount_to_sell + : S.of(context).choose_amount_to_buy, + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: GridView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + // +1 for "custom" option + itemCount: defaultAmounts.length + 1, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, crossAxisSpacing: 8, mainAxisSpacing: 16, mainAxisExtent: 105), + itemBuilder: (context, index) { + final String? item = index == defaultAmounts.length ? null : defaultAmounts[index]; - return BuySellAmountPill( - isLoading: isLoading && item == currentAmount, - amount: item == null ? null : Money.parse(item, fiatCurrency), - onTap: () => onSelected(item), - ); - }), - ), - ], - ); + return BuySellAmountPill( + isLoading: isLoading && item == currentAmount, + amount: item == null ? null : Money.parse(item, fiatCurrency), + onTap: () => onSelected(item), + ); + }), + ), + ], + ); + } } -} -class BuySellAmountPill extends StatelessWidget { - const BuySellAmountPill({super.key, this.amount, required this.onTap, required this.isLoading}); + class BuySellAmountPill extends StatelessWidget { + const BuySellAmountPill({super.key, this.amount, required this.onTap, required this.isLoading}); - final Money? amount; - final VoidCallback onTap; - final bool isLoading; + final Money? amount; + final VoidCallback onTap; + final bool isLoading; - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(9999999999), - border: Border.all( - width: 1, - color: Theme.of(context).colorScheme.surfaceContainerHigh, - ), - gradient: LinearGradient( - colors: [ - context.customColors.cardGradientColorPrimary, - context.customColors.cardGradientColorSecondary - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(9999999999), + border: Border.all( + width: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + gradient: LinearGradient( + colors: [ + context.customColors.cardGradientColorPrimary, + context.customColors.cardGradientColorSecondary + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), ), - ), - child: Material( - color: Colors.transparent, - borderRadius: BorderRadius.circular(9999999999), - child: InkWell( + child: Material( + color: Colors.transparent, borderRadius: BorderRadius.circular(9999999999), - onTap: onTap, - child: isLoading - ? CupertinoActivityIndicator() - : Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - spacing: 4, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (amount != null) + child: InkWell( + borderRadius: BorderRadius.circular(9999999999), + onTap: onTap, + child: isLoading + ? CupertinoActivityIndicator() + : Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + spacing: 4, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (amount != null) + Text( + amount!.toStringWithPrecision(fractionalDigits: 0), + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), + ), Text( - amount!.toStringWithPrecision(fractionalDigits: 0), - style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), - ), + amount?.currency.symbol ?? S.of(context).custom, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: amount == null + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurfaceVariant), + ) + ], + ), + if (amount == null) Text( - amount?.currency.symbol ?? S.of(context).custom, + S.of(context).enter_amount, style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: amount == null - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurfaceVariant), + fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), ) - ], - ), - if (amount == null) - Text( - S.of(context).enter_amount, - style: TextStyle( - fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), - ) - ], - ), + ], + ), + ), ), - ), - ); + ); + } } -} -class BuySellCurrencyPickerPill extends StatelessWidget { - const BuySellCurrencyPickerPill({super.key, required this.curr, required this.onTap}); + class BuySellCurrencyPickerPill extends StatelessWidget { + const BuySellCurrencyPickerPill({super.key, required this.curr, required this.onTap}); - final CryptoCurrency curr; - final VoidCallback onTap; + final CryptoCurrency curr; + final VoidCallback onTap; - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHigh, - borderRadius: BorderRadius.circular(999999999), - ), - child: Padding( - padding: EdgeInsets.only(top: 5, bottom: 5, left: 5, right: 10), - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: 10, - children: [ - CakeImageWidget( - imageUrl: curr.iconPath, - width: 30, - height: 30, - ), - Text( - curr.fullName ?? curr.title, - style: TextStyle(fontSize: 16), - ), - RotatedBox( - quarterTurns: 2, - child: CakeImageWidget( - imageUrl: "assets/new-ui/dropdown_arrow.svg", - width: 8, - height: 8, - colorFilter: - ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn), + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(999999999), + ), + child: Padding( + padding: EdgeInsets.only(top: 5, bottom: 5, left: 5, right: 10), + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 10, + children: [ + CakeImageWidget( + imageUrl: curr.iconPath, + width: 30, + height: 30, ), - ), - ], + Text( + curr.fullName ?? curr.title, + style: TextStyle(fontSize: 16), + ), + RotatedBox( + quarterTurns: 2, + child: CakeImageWidget( + imageUrl: "assets/new-ui/dropdown_arrow.svg", + width: 8, + height: 8, + colorFilter: + ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn), + ), + ), + ], + ), ), ), - ), - ); + ); + } } -} 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 index f965459513..4f16292278 100644 --- a/lib/new-ui/pages/buy_sell/buy_sell_provider_page.dart +++ b/lib/new-ui/pages/buy_sell/buy_sell_provider_page.dart @@ -48,10 +48,9 @@ class _BuySellProviderPageState extends State { leadingIcon: Icon(Icons.arrow_back_ios_new), onLeadingPressed: Navigator.of(context).pop, ), - Expanded( - child: Observer( + Expanded(child: Observer( builder: (_) { - if(widget.buySellViewModel.buySellQuotState is BuySellQuotFailed) { + if (widget.buySellViewModel.buySellQuotState is BuySellQuotFailed) { return Column( mainAxisAlignment: MainAxisAlignment.center, spacing: 24, @@ -64,7 +63,8 @@ class _BuySellProviderPageState extends State { S.of(context).could_not_load_quotes, style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500), ), - Text((widget.buySellViewModel.buySellQuotState as BuySellQuotFailed).errorMessage ?? + Text((widget.buySellViewModel.buySellQuotState as BuySellQuotFailed) + .errorMessage ?? S.of(context).please_try_again_later) ], ) @@ -72,43 +72,56 @@ class _BuySellProviderPageState extends State { ); } - if(widget.buySellViewModel.buySellQuotState is BuySellQuotLoading) { - return Center(child: Row(mainAxisAlignment:MainAxisAlignment.center, spacing:8, children: [ - CupertinoActivityIndicator(), - Text(S.of(context).loading_rates, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),) - ],),); + if (widget.buySellViewModel.buySellQuotState is BuySellQuotLoading) { + return Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 8, + children: [ + CupertinoActivityIndicator(), + Text( + S.of(context).loading_rates, + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), + ) + ], + ), + ); } return Padding( - padding: EdgeInsets.symmetric(horizontal: 18), - child: NewListSections(showHeader: true, sections: { - "": [ - ListItemRegularRow( - keyValue: "payment method", - label: S.of(context).payment_method, - showArrow: true, - onTap: (){ - - final page =BuySellPaymentMethodPage(buySellViewModel: widget.buySellViewModel); - Navigator.of(context).push(CupertinoPageRoute(builder: (context)=>Material(color: Colors.transparent,child: page,))); - }, - trailingText: widget.buySellViewModel.selectedPaymentMethod?.title) - ], - S.of(context).available_providers: [ - ...widget.buySellViewModel.sortedRecommendedQuotes.map(quoteListItem), - ListItemDropdown( - keyValue: "more options", - label: S.of(context).more_options, - onTap: () { - setState(() { - _allProvidersExpanded = !_allProvidersExpanded; - }); - }), - if (_allProvidersExpanded) - ...widget.buySellViewModel.sortedQuotes.map(quoteListItem) - ] - }), - ); + padding: EdgeInsets.symmetric(horizontal: 18), + child: NewListSections(showHeader: true, sections: { + "": [ + ListItemRegularRow( + keyValue: "payment method", + label: S.of(context).payment_method, + showArrow: true, + onTap: () { + final page = + BuySellPaymentMethodPage(buySellViewModel: widget.buySellViewModel); + Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => Material( + color: Colors.transparent, + child: page, + ))); + }, + trailingText: widget.buySellViewModel.selectedPaymentMethod?.title) + ], + S.of(context).available_providers: [ + ...widget.buySellViewModel.sortedRecommendedQuotes.map(quoteListItem), + ListItemDropdown( + keyValue: "more options", + label: S.of(context).more_options, + onTap: () { + setState(() { + _allProvidersExpanded = !_allProvidersExpanded; + }); + }), + if (_allProvidersExpanded) + ...widget.buySellViewModel.sortedQuotes.map(quoteListItem) + ] + }), + ); }, )) ], @@ -116,18 +129,19 @@ class _BuySellProviderPageState extends State { ); } - String get _pageTitle => (widget.buySellViewModel.mode == BuySellPageMode.buy - ? S.current.buy - : S.current.sell) + " " + (widget.buySellViewModel.cryptoCurrency.fullName ?? ""); + String get _pageTitle => + (widget.buySellViewModel.mode == BuySellPageMode.buy ? S.current.buy : S.current.sell) + + " " + + (widget.buySellViewModel.cryptoCurrency.fullName ?? ""); ListItem quoteListItem(Quote quote) => ListItemRegularRow( keyValue: quote.provider.title, label: quote.rampName ?? quote.provider.title, - secondaryLabel: quote.rampName != null? quote.provider.title : null, + secondaryLabel: quote.rampName != null ? quote.provider.title : null, subtitle: quote.badges.isEmpty ? null : quote.badges.join(" - "), subtitleColor: Theme.of(context).colorScheme.primary, iconPath: quote.darkIconPath, - onTap: (){ + onTap: () { widget.buySellViewModel.changeOption(quote); navigateToConfirmation(context); }, @@ -137,14 +151,21 @@ class _BuySellProviderPageState extends State { mainAxisAlignment: MainAxisAlignment.center, spacing: 4, children: [ - Text(widget.buySellViewModel.amountForQuote(quote).toStringWithSymbol(fractionalDigits: 8)), - Text("= ${widget.buySellViewModel.fiatAmountForQuote(quote).toStringWithSymbol(fractionalDigits: 2, trimZeros: false)}", style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant)) - - ],)); + Text(widget.buySellViewModel + .amountForQuote(quote) + .toStringWithSymbol(fractionalDigits: 8)), + Text( + "= ${widget.buySellViewModel.fiatAmountForQuote(quote).toStringWithSymbol(fractionalDigits: 2, trimZeros: false)}", + style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant)) + ], + )); void navigateToConfirmation(BuildContext context) { - final page = BuySellConfirmationPage(buySellViewModel: widget.buySellViewModel); - Navigator.of(context).push(CupertinoPageRoute(builder: (context)=>Material(color: Colors.transparent,child: page,))); + Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => Material( + color: Colors.transparent, + child: page, + ))); } } diff --git a/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart b/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart index 6d5343ccc8..0c10134436 100644 --- a/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart +++ b/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart @@ -14,34 +14,51 @@ class BuySellSelectorModal extends StatelessWidget { @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( - top: false, - child: Column(spacing:24, mainAxisSize: MainAxisSize.min, children: [ - SizedBox.shrink(), - Text(S.of(context).buy_or_sell, style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500),), - Text(S.of(context).buy_or_sell_desc, style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),), - Padding( - padding: EdgeInsets.symmetric(horizontal: 18.0), - child: Column(spacing: 12,children: [ - BuySellSelectorModalButton(title: S.of(context).buy_crypto, description: S.of(context).buy_crypto_desc, iconPath: "assets/new-ui/plus.svg", onTap: ()=>openBuySellPage(context, BuySellPageMode.buy),), - BuySellSelectorModalButton(title: S.of(context).sell_crypto, description: S.of(context).sell_crypto_desc, iconPath: "assets/new-ui/sell.svg", onTap: ()=>openBuySellPage(context, BuySellPageMode.sell)) - ],), + 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, ), - SizedBox.shrink() - ]) - ) - ); + ), + child: SafeArea( + top: false, + child: Column(spacing: 24, mainAxisSize: MainAxisSize.min, children: [ + SizedBox.shrink(), + Text( + S.of(context).buy_or_sell, + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500), + ), + Text( + S.of(context).buy_or_sell_desc, + style: + TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + spacing: 12, + children: [ + BuySellSelectorModalButton( + title: S.of(context).buy_crypto, + description: S.of(context).buy_crypto_desc, + iconPath: "assets/new-ui/plus.svg", + onTap: () => openBuySellPage(context, BuySellPageMode.buy), + ), + BuySellSelectorModalButton( + title: S.of(context).sell_crypto, + description: S.of(context).sell_crypto_desc, + iconPath: "assets/new-ui/sell.svg", + onTap: () => openBuySellPage(context, BuySellPageMode.sell)) + ], + ), + ), + SizedBox.shrink() + ]))); } void openBuySellPage(BuildContext context, BuySellPageMode mode) { @@ -58,9 +75,13 @@ class BuySellSelectorModal extends StatelessWidget { } } - class BuySellSelectorModalButton extends StatelessWidget { - const BuySellSelectorModalButton({super.key, required this.title, required this.description, required this.iconPath, required this.onTap}); + const BuySellSelectorModalButton( + {super.key, + required this.title, + required this.description, + required this.iconPath, + required this.onTap}); final String title; final String description; @@ -75,8 +96,8 @@ class BuySellSelectorModalButton extends StatelessWidget { decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), border: Border.all( - width: 1, color: Theme.of(context).colorScheme.surfaceContainerHigh, - + width: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, ), gradient: LinearGradient( colors: [ @@ -87,16 +108,36 @@ class BuySellSelectorModalButton extends StatelessWidget { end: Alignment.bottomCenter, ), ), - child: Padding(padding: EdgeInsets.all(24), child: Row(spacing: 20, children: [ - - CakeImageWidget(imageUrl: iconPath, width: 55, height: 55, colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant,BlendMode.srcIn),), - Column(spacing: 8, crossAxisAlignment: CrossAxisAlignment.start,children: [ - Text(title, style: TextStyle(fontWeight: FontWeight.w500, fontSize: 16),), - Text(description, style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),) - ],) - - ],),), - + child: Padding( + padding: EdgeInsets.all(24), + child: Row( + spacing: 20, + children: [ + CakeImageWidget( + imageUrl: iconPath, + width: 55, + height: 55, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), + ), + Column( + spacing: 8, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle(fontWeight: FontWeight.w500, fontSize: 16), + ), + Text( + description, + style: TextStyle( + fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), + ) + ], + ) + ], + ), + ), ), ); } diff --git a/lib/new-ui/widgets/coins_page/cards/cards_view.dart b/lib/new-ui/widgets/coins_page/cards/cards_view.dart index ca6cb6bb8c..85812c80c8 100644 --- a/lib/new-ui/widgets/coins_page/cards/cards_view.dart +++ b/lib/new-ui/widgets/coins_page/cards/cards_view.dart @@ -179,7 +179,8 @@ class _CardsViewState extends State { icon: Icons.arrow_forward_ios_rounded, iconSize: 12, onTap: () { - showModalBottomSheet(context: context, builder: (context)=>BuySellSelectorModal()); + showModalBottomSheet( + context: context, builder: (context) => BuySellSelectorModal()); }, ) ] diff --git a/lib/new-ui/widgets/floating_amount_input.dart b/lib/new-ui/widgets/floating_amount_input.dart index 3367d0a3ad..7fb2304523 100644 --- a/lib/new-ui/widgets/floating_amount_input.dart +++ b/lib/new-ui/widgets/floating_amount_input.dart @@ -3,7 +3,14 @@ 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}); + const FloatingAmountInput( + {super.key, + required this.currency, + required this.controller, + this.focusNode, + this.inputFormatters, + this.onChanged, + this.validator}); final Currency currency; final TextEditingController controller; @@ -28,7 +35,7 @@ class _FloatingAmountInputState extends State { @override Widget build(BuildContext context) { - return Center( + return Center( child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.baseline, @@ -59,19 +66,17 @@ class _FloatingAmountInputState extends State { hoverColor: Colors.transparent, focusedBorder: InputBorder.none, enabledBorder: InputBorder.none, - hintText: _amountFocused || widget.controller.text.isNotEmpty - ? null - : "0.00", + 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, - ), + 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, - ), + fontWeight: FontWeight.w400, + fontSize: 45, + color: Theme.of(context).colorScheme.onSurface, + ), ), ), const SizedBox(width: 8), @@ -80,10 +85,10 @@ class _FloatingAmountInputState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.displayMedium?.copyWith( - fontWeight: FontWeight.w400, - fontSize: 45, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + fontWeight: FontWeight.w400, + fontSize: 45, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ], ), diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index e46650517a..fe9c0844ad 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -118,7 +118,7 @@ class ExceptionHandler { if (kDebugMode || kProfileMode) { if (_ignoreError(errorDetails.exception.toString()) || - _ignoreError(errorDetails.stack.toString())) { + _ignoreError(errorDetails.stack.toString()) || _flutterErrorIgnore(errorDetails)) { printV("(BELOW ERROR IS IGNORED AND WILL NOT TRIGGER POPUP IN PROD)"); } FlutterError.presentError(errorDetails); diff --git a/lib/view_model/buy/buy_sell_view_model.dart b/lib/view_model/buy/buy_sell_view_model.dart index 25e8ef8628..4e6e2bf51a 100644 --- a/lib/view_model/buy/buy_sell_view_model.dart +++ b/lib/view_model/buy/buy_sell_view_model.dart @@ -174,10 +174,15 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S return maxAmount.toStringAsFixed(2); } - Money amountForQuote(Quote quote) => Money.parse(double.parse(fiatAmount)/quote.rate, cryptoCurrency); + Money amountForQuote(Quote quote) => + Money.parse(double.parse(fiatAmount) / quote.rate, cryptoCurrency); Money fiatAmountForQuote(Quote quote) { - return Money.parse((fiatConversionStore.prices[cryptoCurrency]! * double.parse(amountForQuote(quote).toString())).toStringAsFixed(2), fiatCurrency); + return Money.parse( + (fiatConversionStore.prices[cryptoCurrency]! * + double.parse(amountForQuote(quote).toString())) + .toStringAsFixed(2), + fiatCurrency); } // based on usd values, should have roughly equal worth (was done with ai though so it's subject to correction) From 72b0844f2950d2781ef4cea9f5c1ef1fac10e429 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Fri, 3 Jul 2026 14:47:25 +0200 Subject: [PATCH 07/11] fix decimals exception --- lib/view_model/buy/buy_sell_view_model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/view_model/buy/buy_sell_view_model.dart b/lib/view_model/buy/buy_sell_view_model.dart index 4e6e2bf51a..4228057cbe 100644 --- a/lib/view_model/buy/buy_sell_view_model.dart +++ b/lib/view_model/buy/buy_sell_view_model.dart @@ -175,7 +175,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S } Money amountForQuote(Quote quote) => - Money.parse(double.parse(fiatAmount) / quote.rate, cryptoCurrency); + Money.parse((double.parse(fiatAmount) / quote.rate).toStringAsFixed(cryptoCurrency.decimals), cryptoCurrency); Money fiatAmountForQuote(Quote quote) { return Money.parse( From 8405aa932c02c1a0d8d993bb33f97b3bee826158 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Fri, 3 Jul 2026 17:00:21 +0200 Subject: [PATCH 08/11] hide currency picker for single-currency wallets --- lib/new-ui/pages/buy_sell/buy_sell_amount_page.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 772cfc77a6..b9a44673f0 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 @@ -222,9 +222,11 @@ Column( spacing: 8, children: [ + if (hasCurrencySelector) ...[ BuySellCurrencyPickerPill(curr: cryptoCurrency, onTap: onCurrencySelectorPressed), SizedBox.shrink(), - FloatingAmountInput( + ], + FloatingAmountInput( currency: fiatCurrency, focusNode: focusNode, controller: controller, @@ -285,6 +287,7 @@ spacing: 24, mainAxisAlignment: MainAxisAlignment.center, children: [ + if(hasCurrencySelector) BuySellCurrencyPickerPill(curr: cryptoCurrency, onTap: onCurrencySelectorPressed), Text( mode == BuySellPageMode.sell From 2c3ef1cf636ba2fd3417e456f8728f1fa177916c Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Fri, 3 Jul 2026 17:00:34 +0200 Subject: [PATCH 09/11] hide "more options" if no more options --- lib/new-ui/pages/buy_sell/buy_sell_provider_page.dart | 1 + 1 file changed, 1 insertion(+) 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 index 4f16292278..1963f9a921 100644 --- a/lib/new-ui/pages/buy_sell/buy_sell_provider_page.dart +++ b/lib/new-ui/pages/buy_sell/buy_sell_provider_page.dart @@ -109,6 +109,7 @@ class _BuySellProviderPageState extends State { ], S.of(context).available_providers: [ ...widget.buySellViewModel.sortedRecommendedQuotes.map(quoteListItem), + if(widget.buySellViewModel.sortedQuotes.isNotEmpty) ListItemDropdown( keyValue: "more options", label: S.of(context).more_options, From c3545b13e316b96740d8659b263da95dbf2f0d19 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Fri, 3 Jul 2026 17:02:13 +0200 Subject: [PATCH 10/11] better error msg --- lib/view_model/buy/buy_sell_view_model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/view_model/buy/buy_sell_view_model.dart b/lib/view_model/buy/buy_sell_view_model.dart index 4228057cbe..0116b943b7 100644 --- a/lib/view_model/buy/buy_sell_view_model.dart +++ b/lib/view_model/buy/buy_sell_view_model.dart @@ -544,7 +544,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S .toList(); if (validQuotes.isEmpty) { - buySellQuotState = BuySellQuotFailed(); + buySellQuotState = BuySellQuotFailed(errorMessage: "No provider could create a quote for ${cryptoCurrency.fullName}"); return; } From 4b40365efb549e7fd4b9d2fe26313411c527886b Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Fri, 3 Jul 2026 17:29:54 +0200 Subject: [PATCH 11/11] add viewpadding for virtual keyboard --- .../pages/buy_sell/buy_sell_amount_page.dart | 79 ++++++++++--------- 1 file changed, 41 insertions(+), 38 deletions(-) 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 b9a44673f0..a0d250facb 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 @@ -215,45 +215,48 @@ @override Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox.shrink(), - Column( - spacing: 8, - children: [ - if (hasCurrencySelector) ...[ - BuySellCurrencyPickerPill(curr: cryptoCurrency, onTap: onCurrencySelectorPressed), - SizedBox.shrink(), - ], - FloatingAmountInput( - currency: fiatCurrency, - focusNode: focusNode, - 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), + return Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox.shrink(), + Column( + spacing: 8, + children: [ + if (hasCurrencySelector) ...[ + BuySellCurrencyPickerPill(curr: cryptoCurrency, onTap: onCurrencySelectorPressed), + SizedBox.shrink(), + ], + FloatingAmountInput( + currency: fiatCurrency, + focusNode: focusNode, + controller: controller, + onChanged: onChanged, ), - ) - ], - ), - Padding( - padding: const EdgeInsets.all(18.0), - child: NewPrimaryButton( - onPressed: onContinuePressed, - isLoading: isLoading, - text: S.of(context).continue_text, - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary), - ) - ], + 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, + isLoading: isLoading, + text: S.of(context).continue_text, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary), + ) + ], + ), ); } }