What changed, and why it matters
This commit is a routine feature update that redesigns the buy/sell cryptocurrency flow in the Cake Wallet app. It adds new screens for selecting amounts, providers, payment methods, confirming trades, and redirecting to third-party payment services. It also updates icons and translations. There is no clear security vulnerability in the code changes themselves, but the commit touches code that handles financial transactions and redirects to external providers, which is always a sensitive area.
No immediate security action is required. As a standard defensive review, recommend verifying that the external provider redirect URLs are validated, that quote/payment-method API responses are properly validated and do not execute dynamic code, and that no sensitive wallet data is leaked to third-party buy/sell providers. A normal code-quality and integration test pass is appropriate.
Security signals we found
Financial transaction flow modified (buy/sell crypto)
External provider redirection logic present (launchTrade)
New UI state management for quotes and payment methods
No explicit security fix or vulnerability mention in commit
No input sanitization or secure coding anti-patterns visible in diff
Evidence from the diff
The commit refactors the buy/sell UI/UX in a Flutter/Dart wallet application. It introduces new pages (amount, provider, payment method, confirmation, redirecting), updates the view model with new computed properties for quote amounts and currency selection, and adds new SVG payment-method assets. The changes include navigation to external trade launch URLs, loading of quotes/payment methods, and display of fiat/crypto conversion amounts. No explicit security fixes or vulnerability disclosures are present in the commit message or diff. The code does not introduce obvious injection, cryptographic, or authentication flaws based on the diff alone, but the full backend/provider integration logic is not shown.
Changed components
lib/new-ui/pages/buy_sell/*lib/view_model/buy/buy_sell_view_model.dartlib/buy/buy_quote.dartlib/buy/payment_method.dartlib/di.dartlib/src/widgets/cake_image_widget.dartassets and localization filesInspect captured patch +926 / −125
diff --git a/assets/images/skrill.svg b/assets/images/skrill.svg
index b264b57e..542c00c1 100644
--- a/assets/images/skrill.svg
+++ b/assets/images/skrill.svg
@@ -1,9 +1,9 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
<defs>
</defs>
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
- <circle cx="45" cy="45" r="45" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(127,33,99); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
+ <rect x="0" y="0" width="90" height="90" rx="10" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(127,33,99); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
<polygon points="69.59,36.9 69.59,54.86 74.5,54.86 74.5,36.02 " style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
<polygon points="62.42,36.9 67.33,36.02 67.33,54.87 62.42,54.87 " style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
<rect x="55.43" y="41.08" rx="0" ry="0" width="4.91" height="13.78" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
diff --git a/lib/buy/buy_quote.dart b/lib/buy/buy_quote.dart
index 1e154de6..ee301e82 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 06f1cfe3..b6b23093 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<String, dynamic> json) {
diff --git a/lib/di.dart b/lib/di.dart
index faf6196a..4439a68b 100644
--- a/lib/di.dart
+++ b/lib/di.dart
@@ -1495,7 +1495,7 @@ Future<void> setup({
getIt.registerFactory(() => BuyAmountViewModel());
getIt.registerFactoryParam<BuySellViewModel, BuySellPageMode, void>(
- (mode, _) => BuySellViewModel(mode: mode, getIt.get<AppStore>()));
+ (mode, _) => BuySellViewModel(mode: mode, getIt.get<AppStore>(), fiatConversionStore: getIt.get<FiatConversionStore>()));
getIt.registerFactoryParam<NewBuySellAmountPage, BuySellPageMode, void>((mode, _) =>
NewBuySellAmountPage(buySellViewModel: getIt.get<BuySellViewModel>(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 4660199f..bfa2571d 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 32700893..ec22421b 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<NewBuySellAmountPage> {
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<NewBuySellAmountPage> {
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<NewBuySellAmountPage> {
);
}
+ 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<void> 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<String> 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 00000000..5de696f7
--- /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 00000000..e1ca3f44
--- /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 acbc7f71..f9654595 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<BuySellProviderPage> createState() => _BuySellProviderPageState();
+}
+
+class _BuySellProviderPageState extends State<BuySellProviderPage> {
+ 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 00000000..f96531b8
--- /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<BuySellRedirectingPage> createState() => _BuySellRedirectingPageState();
+}
+
+class _BuySellRedirectingPageState extends State<BuySellRedirectingPage> {
+ 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 7e6efd00..6d5343cc 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<NewBuySellAmountPage>(param1: mode), parentContext: context,));
+ final page = getIt.get<NewBuySellAmountPage>(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 b41eb9c2..d345156d 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 f714bcd0..5b865324 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 011f90ea..1a0ef17d 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 b2301b88..dc552fd6 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 8abef379..e5097e0a 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 100f1332..25e8ef86 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<BuyProvider> 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<double>(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<FiatCurrency, List<String>> _defaultAmountsMap = {
@@ -252,6 +277,13 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency);
}
+
+ @computed
+ Iterable<CryptoCurrency> 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<BuyProvider> 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 c5525007..c4604372 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 00000000..b3e2adad
--- /dev/null
+++ b/res/pictures/buy_payment_methods/all_methods.svg
@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M13.7514 12.7112C13.0527 12.7112 12.4614 12.4692 11.9774 11.9852C11.4934 11.5012 11.2514 10.9099 11.2514 10.2112C11.2514 9.51257 11.4934 8.92123 11.9774 8.43723C12.4614 7.95323 13.0527 7.71123 13.7514 7.71123C14.4501 7.71123 15.0414 7.95323 15.5254 8.43723C16.0094 8.92123 16.2514 9.51257 16.2514 10.2112C16.2514 10.9099 16.0094 11.5012 15.5254 11.9852C15.0414 12.4692 14.4501 12.7112 13.7514 12.7112ZM7.15516 15.8075C6.65816 15.8075 6.23266 15.6305 5.87866 15.2765C5.52466 14.9225 5.34766 14.4969 5.34766 13.9997V6.42273C5.34766 5.92573 5.52466 5.50023 5.87866 5.14623C6.23266 4.79223 6.65816 4.61523 7.15516 4.61523H20.3474C20.8446 4.61523 21.2702 4.79223 21.6242 5.14623C21.9782 5.50023 22.1552 5.92573 22.1552 6.42273V13.9997C22.1552 14.4969 21.9782 14.9225 21.6242 15.2765C21.2702 15.6305 20.8446 15.8075 20.3474 15.8075H7.15516ZM8.65516 14.3075H18.8477C18.8477 13.8088 19.0247 13.3828 19.3787 13.0295C19.7327 12.6763 20.1582 12.4997 20.6552 12.4997V7.92273C20.1565 7.92273 19.7306 7.74573 19.3774 7.39173C19.0242 7.03773 18.8477 6.61223 18.8477 6.11523H8.65516C8.65516 6.6139 8.47816 7.03982 8.12416 7.39298C7.77016 7.74615 7.34466 7.92273 6.84766 7.92273V12.4997C7.34632 12.4997 7.77224 12.6767 8.12541 13.0307C8.47857 13.3847 8.65516 13.8103 8.65516 14.3075ZM19.1744 19.3075H3.65541C3.15824 19.3075 2.73266 19.1305 2.37866 18.7765C2.02466 18.4225 1.84766 17.9969 1.84766 17.4997V7.59598H3.34766V17.4997C3.34766 17.5766 3.37966 17.6471 3.44366 17.7112C3.50782 17.7754 3.57841 17.8075 3.65541 17.8075H19.1744V19.3075ZM7.15516 14.3075H6.84766V6.11523H7.15516C7.07182 6.11523 6.99974 6.14565 6.93891 6.20648C6.87807 6.26732 6.84766 6.3394 6.84766 6.42273V13.9997C6.84766 14.0831 6.87807 14.1552 6.93891 14.216C6.99974 14.277 7.07182 14.3075 7.15516 14.3075Z" fill="white"/>
+</svg>
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 00000000..dd7f18b6
--- /dev/null
+++ b/res/pictures/buy_payment_methods/apple_pay.svg
@@ -0,0 +1,8 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <rect width="24" height="24" rx="4.8" fill="black"/>
+ <path d="M6.59433 9.42593C6.80343 9.16438 6.94533 8.81321 6.90791 8.45435C6.60181 8.46957 6.22827 8.65629 6.01201 8.91804C5.81783 9.14219 5.64597 9.50807 5.69077 9.85189C6.03439 9.88169 6.37768 9.68014 6.59433 9.42593Z" fill="white"/>
+ <path d="M6.90469 9.91849C6.40568 9.88877 5.9814 10.2017 5.7431 10.2017C5.50466 10.2017 5.13974 9.93347 4.74506 9.9407C4.23134 9.94825 3.75468 10.2387 3.49399 10.7007C2.9578 11.6248 3.35249 12.9957 3.87391 13.7483C4.12712 14.1207 4.43228 14.5307 4.83438 14.516C5.21429 14.5011 5.36321 14.27 5.82501 14.27C6.28647 14.27 6.42061 14.516 6.82278 14.5085C7.23983 14.5011 7.50056 14.136 7.75377 13.7632C8.04425 13.3388 8.16317 12.9289 8.17064 12.9064C8.16316 12.8989 7.36645 12.5933 7.35906 11.6768C7.35154 10.9094 7.98457 10.5444 8.01436 10.5218C7.65688 9.99309 7.09833 9.93347 6.90469 9.91849Z" fill="white"/>
+ <path d="M11.248 8.88098C12.3326 8.88098 13.0878 9.6286 13.0878 10.7171C13.0878 11.8094 12.317 12.5609 11.2208 12.5609H10.02V14.4706H9.15234V8.88098H11.248V8.88098ZM10.02 11.8327H11.0155C11.7709 11.8327 12.2008 11.426 12.2008 10.721C12.2008 10.016 11.7709 9.61313 11.0194 9.61313H10.02V11.8327Z" fill="white"/>
+ <path d="M13.3125 13.3125C13.3125 12.5997 13.8587 12.162 14.8272 12.1078L15.9427 12.0419V11.7282C15.9427 11.275 15.6366 11.0038 15.1254 11.0038C14.6411 11.0038 14.339 11.2362 14.2654 11.6004H13.4752C13.5217 10.8643 14.1492 10.322 15.1564 10.322C16.1441 10.322 16.7755 10.845 16.7755 11.6623V14.4707H15.9736V13.8005H15.9543C15.7181 14.2538 15.2028 14.5404 14.6683 14.5404C13.8703 14.5404 13.3125 14.0446 13.3125 13.3125ZM15.9427 12.9445V12.623L14.9394 12.6849C14.4397 12.7198 14.1569 12.9406 14.1569 13.2892C14.1569 13.6456 14.4513 13.878 14.9007 13.878C15.4856 13.878 15.9427 13.4751 15.9427 12.9445Z" fill="white"/>
+ <path d="M17.5357 15.9698V15.2919C17.5976 15.3074 17.737 15.3074 17.8068 15.3074C18.1941 15.3074 18.4033 15.1447 18.5311 14.7264C18.5311 14.7186 18.6047 14.4785 18.6047 14.4746L17.1328 10.3956H18.0391L19.0696 13.7115H19.085L20.1155 10.3956H20.9987L19.4723 14.6837C19.1239 15.6715 18.721 15.9891 17.8765 15.9891C17.8068 15.9891 17.5976 15.9814 17.5357 15.9698Z" fill="white"/>
+</svg>
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 00000000..2eccd60c
--- /dev/null
+++ b/res/pictures/buy_payment_methods/bank_transfer.svg
@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M5.75028 16.9999V9.49987H7.25028V16.9999H5.75028ZM11.2503 16.9999V9.49987H12.7503V16.9999H11.2503ZM2.76953 20.4999V18.9999H21.231V20.4999H2.76953ZM16.7503 16.9999V9.49987H18.2503V16.9999H16.7503ZM2.76953 7.49987V6.07687L12.0003 1.55762L21.231 6.07687V7.49987H2.76953Z" fill="white"/>
+</svg>
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 00000000..4b68f70c
--- /dev/null
+++ b/res/pictures/buy_payment_methods/debit_card.svg
@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M21.5 6.30775V17.6923C21.5 18.1974 21.325 18.625 20.975 18.975C20.625 19.325 20.1974 19.5 19.6923 19.5H4.30775C3.80258 19.5 3.375 19.325 3.025 18.975C2.675 18.625 2.5 18.1974 2.5 17.6923V6.30775C2.5 5.80258 2.675 5.375 3.025 5.025C3.375 4.675 3.80258 4.5 4.30775 4.5H19.6923C20.1974 4.5 20.625 4.675 20.975 5.025C21.325 5.375 21.5 5.80258 21.5 6.30775ZM4 8.404H20V6.30775C20 6.23075 19.9679 6.16025 19.9038 6.09625C19.8398 6.03208 19.7693 6 19.6923 6H4.30775C4.23075 6 4.16025 6.03208 4.09625 6.09625C4.03208 6.16025 4 6.23075 4 6.30775V8.404ZM4 11.596V17.6923C4 17.7692 4.03208 17.8398 4.09625 17.9038C4.16025 17.9679 4.23075 18 4.30775 18H19.6923C19.7693 18 19.8398 17.9679 19.9038 17.9038C19.9679 17.8398 20 17.7692 20 17.6923V11.596H4Z" fill="#8C9FBB"/>
+</svg>
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 00000000..836c8acb
--- /dev/null
+++ b/res/pictures/buy_payment_methods/google_pay.svg
@@ -0,0 +1,11 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <rect x="0.15" y="0.15" width="23.7" height="23.7" rx="4.65" fill="white"/>
+ <rect x="0.15" y="0.15" width="23.7" height="23.7" rx="4.65" stroke="#BBBFCC" stroke-width="0.3"/>
+ <path d="M11.2976 12.6748V14.9021H10.5859V9.40186H12.4725C12.9507 9.40186 13.3584 9.56016 13.692 9.87678C14.033 10.1934 14.2034 10.58 14.2034 11.0365C14.2034 11.504 14.033 11.8906 13.692 12.2035C13.3621 12.5164 12.9544 12.6711 12.4725 12.6711H11.2976V12.6748ZM11.2976 10.0793V11.9973H12.4874C12.769 11.9973 13.0063 11.9016 13.1916 11.7139C13.3806 11.5261 13.477 11.2979 13.477 11.0401C13.477 10.7861 13.3806 10.5615 13.1916 10.3738C13.0063 10.1787 12.7728 10.0829 12.4874 10.0829H11.2976V10.0793Z" fill="#3C4043"/>
+ <path d="M16.0632 11.0151C16.5896 11.0151 17.0047 11.155 17.3086 11.4348C17.6126 11.7146 17.7645 12.0975 17.7645 12.5835V14.9029H17.0862V14.3801H17.0566C16.7638 14.8108 16.3709 15.0243 15.8816 15.0243C15.4628 15.0243 15.1144 14.9029 14.8327 14.6562C14.551 14.4095 14.4102 14.104 14.4102 13.7358C14.4102 13.3456 14.5584 13.0363 14.8549 12.808C15.1515 12.5761 15.548 12.462 16.041 12.462C16.4635 12.462 16.812 12.5393 17.0825 12.6939V12.5319C17.0825 12.2853 16.9862 12.0791 16.7897 11.9061C16.5933 11.733 16.3635 11.6484 16.1003 11.6484C15.7037 11.6484 15.3887 11.814 15.1589 12.1491L14.5325 11.7588C14.8772 11.2618 15.3887 11.0151 16.0632 11.0151ZM15.144 13.7468C15.144 13.9309 15.2219 14.0855 15.3813 14.207C15.5369 14.3285 15.7222 14.3911 15.9335 14.3911C16.2337 14.3911 16.5006 14.2807 16.7341 14.0598C16.9676 13.8389 17.0862 13.5812 17.0862 13.283C16.8638 13.1099 16.5562 13.0216 16.1596 13.0216C15.8705 13.0216 15.6296 13.0915 15.4369 13.2277C15.2404 13.3713 15.144 13.5444 15.144 13.7468Z" fill="#3C4043"/>
+ <path d="M21.6357 11.1357L19.2635 16.555H18.5296L19.4118 14.659L17.8477 11.1357H18.6223L19.7491 13.838H19.7639L20.861 11.1357H21.6357Z" fill="#3C4043"/>
+ <path d="M8.46805 12.2255C8.46805 11.9951 8.44729 11.7746 8.40875 11.5625H5.42578V12.7774L7.14373 12.7778C7.07405 13.182 6.84981 13.5266 6.50622 13.7563V14.5446H7.52883C8.12594 13.9956 8.46805 13.1842 8.46805 12.2255Z" fill="#4285F4"/>
+ <path d="M6.50368 13.7559C6.21902 13.9466 5.85245 14.0581 5.42361 14.0581C4.59521 14.0581 3.89247 13.5037 3.6408 12.7563H2.58594V13.5692C3.10855 14.5993 4.18268 15.3062 5.42361 15.3062C6.28129 15.3062 7.00183 15.026 7.52629 14.5437L6.50368 13.7559Z" fill="#34A853"/>
+ <path d="M3.54319 12.1526C3.54319 11.9428 3.5784 11.7399 3.64252 11.5492V10.7363H2.58766C2.37157 11.1623 2.25 11.6431 2.25 12.1526C2.25 12.6621 2.37194 13.143 2.58766 13.5689L3.64252 12.756C3.5784 12.5653 3.54319 12.3625 3.54319 12.1526Z" fill="#FABB05"/>
+ <path d="M5.42361 10.2478C5.89174 10.2478 6.31094 10.4079 6.64193 10.7209L7.54816 9.82148C6.99775 9.31232 6.28017 8.99976 5.42361 8.99976C4.18305 8.99976 3.10855 9.70661 2.58594 10.7367L3.6408 11.5496C3.89247 10.8022 4.59521 10.2478 5.42361 10.2478Z" fill="#E94235"/>
+</svg>
diff --git a/res/pictures/buy_payment_methods/paypal.svg b/res/pictures/buy_payment_methods/paypal.svg
new file mode 100644
index 00000000..d723ede3
--- /dev/null
+++ b/res/pictures/buy_payment_methods/paypal.svg
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg">
+ <defs
+ id="defs5"/>
+ <path
+ clip-path="none"
+ d="M-837.663-237.968a5.49 5.49 0 0 0-5.423 4.633l-9.013 57.15-8.281 52.514-.005.044.01-.044 8.281-52.514c.421-2.669 2.719-4.633 5.42-4.633h26.404c26.573 0 49.127-19.387 53.246-45.658.314-1.996.482-3.973.52-5.924v-.003h-.003c-6.753-3.543-14.683-5.565-23.372-5.565z"
+ fill="#001c64"
+ id="path1"/>
+ <path
+ clip-path="none"
+ d="M-766.506-232.402c-.037 1.951-.207 3.93-.52 5.926-4.119 26.271-26.673 45.658-53.246 45.658h-26.404c-2.701 0-4.999 1.964-5.42 4.633l-8.281 52.514-5.197 32.947a4.46 4.46 0 0 0 4.405 5.153h28.66a5.49 5.49 0 0 0 5.423-4.633l7.55-47.881c.423-2.669 2.722-4.636 5.423-4.636h16.876c26.573 0 49.124-19.386 53.243-45.655 2.924-18.649-6.46-35.614-22.511-44.026z"
+ fill="#0070e0"
+ id="path2"/>
+ <path
+ clip-path="none"
+ d="M-870.225-276.071a5.49 5.49 0 0 0-5.423 4.636l-22.489 142.608a4.46 4.46 0 0 0 4.405 5.156h33.351l8.281-52.514 9.013-57.15a5.49 5.49 0 0 1 5.423-4.633h47.782c8.691 0 16.621 2.025 23.375 5.563.46-23.917-19.275-43.666-46.412-43.666z"
+ fill="#003087"
+ id="path3"/>
+ <rect
+ x="0.312854"
+ y="0.312854"
+ width="23.3743"
+ height="23.3743"
+ rx="4.48715"
+ fill="white"
+ id="rect3"/>
+ <g
+ transform="matrix(0.08353969,0,0,0.08353969,80.638168,27.012036)"
+ id="g3">
+ <path
+ clip-path="none"
+ d="m -837.663,-237.968 a 5.49,5.49 0 0 0 -5.423,4.633 l -9.013,57.15 -8.281,52.514 -0.005,0.044 0.01,-0.044 8.281,-52.514 c 0.421,-2.669 2.719,-4.633 5.42,-4.633 h 26.404 c 26.573,0 49.127,-19.387 53.246,-45.658 0.314,-1.996 0.482,-3.973 0.52,-5.924 v -0.003 h -0.003 c -6.753,-3.543 -14.683,-5.565 -23.372,-5.565 z"
+ fill="#001c64"
+ id="path1-8"/>
+ <path
+ clip-path="none"
+ d="m -766.506,-232.402 c -0.037,1.951 -0.207,3.93 -0.52,5.926 -4.119,26.271 -26.673,45.658 -53.246,45.658 h -26.404 c -2.701,0 -4.999,1.964 -5.42,4.633 l -8.281,52.514 -5.197,32.947 a 4.46,4.46 0 0 0 4.405,5.153 h 28.66 a 5.49,5.49 0 0 0 5.423,-4.633 l 7.55,-47.881 c 0.423,-2.669 2.722,-4.636 5.423,-4.636 h 16.876 c 26.573,0 49.124,-19.386 53.243,-45.655 2.924,-18.649 -6.46,-35.614 -22.511,-44.026 z"
+ fill="#0070e0"
+ id="path2-0"/>
+ <path
+ clip-path="none"
+ d="m -870.225,-276.071 a 5.49,5.49 0 0 0 -5.423,4.636 l -22.489,142.608 a 4.46,4.46 0 0 0 4.405,5.156 h 33.351 l 8.281,-52.514 9.013,-57.15 a 5.49,5.49 0 0 1 5.423,-4.633 h 47.782 c 8.691,0 16.621,2.025 23.375,5.563 0.46,-23.917 -19.275,-43.666 -46.412,-43.666 z"
+ fill="#003087"
+ id="path3-2"/>
+ </g>
+ <rect
+ x="0.312854"
+ y="0.312854"
+ width="23.3743"
+ height="23.3743"
+ rx="4.48715"
+ stroke="#DBDBDB"
+ stroke-width="0.625708"
+ id="rect4"/>
+ <rect
+ x="2.96484"
+ y="3.05762"
+ width="17.5198"
+ height="17.5198"
+ fill="url(#pattern0_18189_347672)"
+ id="rect5"/>
+</svg>
diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb
index 207ee288..9929af11 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",
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.