new buy/sell

This commit is contained in:
Robert Malikowski
2026-07-03 12:54:07 +02:00
parent afbc6db066
commit 72c2899195
25 changed files with 926 additions and 125 deletions
+3 -2
View File
@@ -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;
}
}
}
+15 -5
View File
@@ -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) {
+1 -1
View File
@@ -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)));
@@ -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;
@@ -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),
)
],
),
),
@@ -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,
)));
}
}
@@ -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()
}),
),
))
],
)),
);
}
}
@@ -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,)));
}
}
@@ -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()
],
),
);
},
),
),
);
}
}
@@ -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,
));
}
}
@@ -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(
+3 -2
View File
@@ -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),
);
@@ -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,
),
],
),
],
);
},
);
@@ -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),
),
],
),
@@ -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,
+51 -7
View File
@@ -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;
}