From 72c2899195f657580d049208ce4438e4fba01717 Mon Sep 17 00:00:00 2001 From: Robert Malikowski Date: Fri, 3 Jul 2026 12:54:07 +0200 Subject: [PATCH] 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 b264b57eb..542c00c12 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 1e154de6b..ee301e82f 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 06f1cfe34..b6b23093d 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 faf6196af..4439a68bd 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 4660199ff..bfa2571d8 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 327008938..ec22421b1 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 000000000..5de696f7b --- /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 000000000..e1ca3f449 --- /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 acbc7f71e..f96545951 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 000000000..f96531b82 --- /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 7e6efd004..6d5343ccc 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 b41eb9c20..d345156dd 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 f714bcd0c..5b865324c 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 011f90ea9..1a0ef17de 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 b2301b881..dc552fd66 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 8abef379d..e5097e0a7 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 100f13329..25e8ef862 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 c5525007b..c46043726 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 000000000..b3e2adade --- /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 000000000..dd7f18b62 --- /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 000000000..2eccd60ca --- /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 000000000..4b68f70c6 --- /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 000000000..836c8acb1 --- /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 000000000..d723ede3e --- /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 207ee288c..9929af118 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",