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_provider.dart b/lib/buy/buy_provider.dart index 7cc92eb9b1..d2a4bc9661 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}) => null; Future requestUrl(String amount, String sourceCurrency) => throw UnimplementedError(); 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 821f51130a..4439a68bd7 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'; @@ -1492,7 +1494,11 @@ Future setup({ getIt.registerFactory(() => BuyAmountViewModel()); - getIt.registerFactory(() => BuySellViewModel(getIt.get())); + getIt.registerFactoryParam( + (mode, _) => BuySellViewModel(mode: mode, getIt.get(), fiatConversionStore: getIt.get())); + + getIt.registerFactoryParam((mode, _) => + NewBuySellAmountPage(buySellViewModel: getIt.get(param1: mode))); getIt.registerFactory(() => BuySellPage(getIt.get())); 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 new file mode 100644 index 0000000000..a0d250facb --- /dev/null +++ b/lib/new-ui/pages/buy_sell/buy_sell_amount_page.dart @@ -0,0 +1,441 @@ + 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}); + + final BuySellViewModel buySellViewModel; + + @override + State createState() => _NewBuySellAmountPageState(); + } + + 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) { + 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, + ), + ), + 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; + }); + + 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 { + 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; + + @override + Widget build(BuildContext context) { + 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, + ), + 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}); + + 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: [ + if(hasCurrencySelector) + 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), + ); + }), + ), + ], + ); + } + } + + 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; + + @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: 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), + ) + ], + ), + 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), + ), + ), + ], + ), + ), + ), + ); + } + } 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 new file mode 100644 index 0000000000..1963f9a921 --- /dev/null +++ b/lib/new-ui/pages/buy_sell/buy_sell_provider_page.dart @@ -0,0 +1,172 @@ +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 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( + 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: (_) { + 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), + if(widget.buySellViewModel.sortedQuotes.isNotEmpty) + ListItemDropdown( + keyValue: "more options", + label: S.of(context).more_options, + onTap: () { + setState(() { + _allProvidersExpanded = !_allProvidersExpanded; + }); + }), + if (_allProvidersExpanded) + ...widget.buySellViewModel.sortedQuotes.map(quoteListItem) + ] + }), + ); + }, + )) + ], + )), + ); + } + + 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, + 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 new file mode 100644 index 0000000000..0c10134436 --- /dev/null +++ b/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart @@ -0,0 +1,144 @@ +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/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'; + +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(); + final page = getIt.get(param1: mode); + showModalBottomSheet( + useSafeArea: true, + isScrollControlled: true, + context: context, + builder: (modalContext) => ModalNavigator( + rootPage: page, + parentContext: context, + )); + } +} + +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..85812c80c8 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,10 @@ 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/lib/new-ui/widgets/floating_amount_input.dart b/lib/new-ui/widgets/floating_amount_input.dart new file mode 100644 index 0000000000..7fb2304523 --- /dev/null +++ b/lib/new-ui/widgets/floating_amount_input.dart @@ -0,0 +1,97 @@ +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/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/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/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/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 a299aebac8..0116b943b7 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'; @@ -11,10 +12,14 @@ 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/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'; import 'package:flutter/cupertino.dart'; import 'package:mobx/mobx.dart'; @@ -25,6 +30,7 @@ class BuySellViewModel = BuySellViewModelBase with _$BuySellViewModel; abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with Store { BuySellViewModelBase( AppStore appStore, + {required this.mode, required this.fiatConversionStore} ) : _cryptoAmount = '', fiatAmount = '', cryptoCurrencyAddress = '', @@ -56,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 @@ -81,7 +89,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 +108,8 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S @observable List fiatCurrencies; - @observable - bool isBuyAction = true; + final BuySellPageMode mode; + @observable List providerList; @@ -149,6 +157,85 @@ 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).toStringAsFixed(cryptoCurrency.decimals), 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 = { + 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; @@ -181,11 +268,6 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S _initialize(); } - @action - void changeBuySellAction() { - isBuyAction = !isBuyAction; - _initialize(); - } @action void changeFiatCurrency({required FiatCurrency currency}) { @@ -200,6 +282,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; @@ -214,16 +303,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 { @@ -338,7 +429,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S } void _setProviders() => - providerList = isBuyAction ? availableBuyProviders : availableSellProviders; + providerList = mode == BuySellPageMode.buy ? availableBuyProviders : availableSellProviders; Future _initialize() async { _setProviders(); @@ -347,6 +438,9 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S cryptoCurrencyAddress = _getInitialCryptoCurrencyAddress(); paymentMethodState = InitialPaymentMethod(); buySellQuotState = InitialBuySellQuotState(); + sortedRecommendedQuotes.clear(); + sortedQuotes.clear(); + paymentMethods.clear(); await _getAvailablePaymentTypes(); await calculateBestRate(); } @@ -367,7 +461,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: () => [], ))); @@ -403,17 +497,26 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S buySellQuotState = BuySellQuotLoading(); final List validProviders = providerList.where((provider) { - if (isBuyAction) { - return provider.supportedCryptoList - .any((pair) => pair.from == cryptoCurrency && pair.to == fiatCurrency); + if (mode == BuySellPageMode.buy) { + 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; } @@ -423,7 +526,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, ) @@ -441,11 +544,11 @@ 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; } - 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)); @@ -488,7 +591,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; }); @@ -510,7 +613,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S context: context, quote: selectedQuote!, amount: amount, - isBuyAction: isBuyAction, + isBuyAction: mode == BuySellPageMode.buy, cryptoCurrencyAddress: cryptoCurrencyAddress, ); } catch (e) { 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/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..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", @@ -135,7 +136,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", @@ -195,6 +200,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:", @@ -248,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", @@ -273,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", @@ -474,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.", @@ -532,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", @@ -608,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", @@ -759,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)", @@ -790,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", @@ -812,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", @@ -831,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", @@ -988,6 +1005,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", @@ -1306,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",