What changed, and why it matters
This commit is a work-in-progress (WIP) refactor of the buy/sell cryptocurrency flow in the Cake Wallet app. It restructures how the buy/sell mode is passed through the dependency injection system, replaces some UI placeholders, and adds a new custom amount input widget. There is no indication of a security fix or vulnerability being addressed.
No security action required. This appears to be routine feature/refactoring work. Continue normal code review and testing.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit refactors BuySellViewModel to take a required BuySellPageMode parameter instead of a mutable isBuyAction observable, and updates dependency injection registrations accordingly. It introduces a FloatingAmountInput widget and a new BuySellCustomAmountInput/BuySellProviderPage UI, while replacing the existing exchange card sections with Placeholder widgets. Several debug printV statements are added. No security-relevant changes are evident.
Changed components
lib/di.dartlib/new-ui/pages/buy_sell/buy_sell_amount_page.dartlib/new-ui/pages/buy_sell/buy_sell_provider_page.dartlib/new-ui/widgets/floating_amount_input.dartlib/src/screens/buy/buy_sell_page.dartlib/view_model/buy/buy_sell_view_model.dartInspect captured patch +255 / −75
diff --git a/lib/di.dart b/lib/di.dart
index a4ae25ca..faf6196a 100644
--- a/lib/di.dart
+++ b/lib/di.dart
@@ -1494,10 +1494,11 @@ Future<void> setup({
getIt.registerFactory(() => BuyAmountViewModel());
- getIt.registerFactory(() => BuySellViewModel(getIt.get<AppStore>()));
+ getIt.registerFactoryParam<BuySellViewModel, BuySellPageMode, void>(
+ (mode, _) => BuySellViewModel(mode: mode, getIt.get<AppStore>()));
getIt.registerFactoryParam<NewBuySellAmountPage, BuySellPageMode, void>((mode, _) =>
- NewBuySellAmountPage(mode: mode, buySellViewModel: getIt.get<BuySellViewModel>()));
+ NewBuySellAmountPage(buySellViewModel: getIt.get<BuySellViewModel>(param1: mode)));
getIt.registerFactory(() => BuySellPage(getIt.get<BuySellViewModel>()));
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 3b36c583..32700893 100644
--- a/lib/new-ui/pages/buy_sell/buy_sell_amount_page.dart
+++ b/lib/new-ui/pages/buy_sell/buy_sell_amount_page.dart
@@ -1,16 +1,19 @@
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart';
+import 'package:cake_wallet/new-ui/widgets/floating_amount_input.dart';
+import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/themes/core/theme_extension.dart';
import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart';
import 'package:cw_core/amount/money.dart';
+import 'package:cw_core/crypto_currency.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_mobx/flutter_mobx.dart';
class NewBuySellAmountPage extends StatefulWidget {
- const NewBuySellAmountPage({super.key, required this.mode, required this.buySellViewModel});
+ const NewBuySellAmountPage({super.key, required this.buySellViewModel});
- final BuySellPageMode mode;
final BuySellViewModel buySellViewModel;
@override
@@ -19,6 +22,7 @@ class NewBuySellAmountPage extends StatefulWidget {
class _NewBuySellAmountPageState extends State<NewBuySellAmountPage> {
bool _customAmountMode = false;
+ final customInputController = TextEditingController();
@override
Widget build(BuildContext context) {
@@ -40,22 +44,37 @@ class _NewBuySellAmountPageState extends State<NewBuySellAmountPage> {
ModalTopBar(
title: _pageTitle,
leadingIcon: Icon(Icons.close),
- onLeadingPressed: Navigator.of(context).pop,
+ onLeadingPressed: Navigator.of(context, rootNavigator: true).pop,
),
Expanded(
- child: BuySellDefaultAmountSelector(
- defaultAmounts: widget.buySellViewModel.defaultAmounts,
- currency: widget.buySellViewModel.fiatCurrency,
- mode: widget.mode,
- onSelected: (amount) {
- if (amount == null) {
- setState(() {
- _customAmountMode = true;
- });
- } else {
- widget.buySellViewModel.changeFiatAmount(amount: amount);
- }
- },
+ child: AnimatedSwitcher(
+ duration: Duration(milliseconds: 300),
+ child: _customAmountMode
+ ? Observer(
+ builder: (_) => BuySellCustomAmountInput(
+ fiatCurrency: widget.buySellViewModel.fiatCurrency,
+ cryptoCurrency: widget.buySellViewModel.cryptoCurrency,
+ cryptoAmount: widget.buySellViewModel.cryptoAmount,
+ controller: customInputController,
+ onContinuePressed: () {},
+ onChanged: (amount) =>
+ widget.buySellViewModel.changeFiatAmount(amount: amount),
+ ))
+ : BuySellDefaultAmountSelector(
+ key: ValueKey(0),
+ defaultAmounts: widget.buySellViewModel.defaultAmounts,
+ currency: widget.buySellViewModel.fiatCurrency,
+ mode: widget.buySellViewModel.mode,
+ onSelected: (amount) {
+ if (amount == null) {
+ setState(() {
+ _customAmountMode = true;
+ });
+ } else {
+ widget.buySellViewModel.changeFiatAmount(amount: amount);
+ }
+ },
+ ),
))
],
),
@@ -63,7 +82,7 @@ class _NewBuySellAmountPageState extends State<NewBuySellAmountPage> {
);
}
- String get _pageTitle => widget.mode == BuySellPageMode.buy
+ String get _pageTitle => widget.buySellViewModel.mode == BuySellPageMode.buy
? S.current.buy
: S.current.sell +
((widget.buySellViewModel.cryptoCurrencies.length == 1)
@@ -71,6 +90,62 @@ class _NewBuySellAmountPageState extends State<NewBuySellAmountPage> {
: "");
}
+class BuySellCustomAmountInput extends StatelessWidget {
+ const BuySellCustomAmountInput(
+ {super.key,
+ required this.fiatCurrency,
+ required this.cryptoCurrency,
+ required this.cryptoAmount,
+ required this.controller,
+ required this.onContinuePressed,
+ required this.onChanged});
+
+ final FiatCurrency fiatCurrency;
+ final CryptoCurrency cryptoCurrency;
+ final String cryptoAmount;
+ final TextEditingController controller;
+ final VoidCallback onContinuePressed;
+ final Function(String) onChanged;
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ SizedBox.shrink(),
+ Column(
+ spacing: 8,
+ children: [
+ FloatingAmountInput(
+ currency: fiatCurrency,
+ controller: controller,
+ onChanged: onChanged,
+ ),
+ Opacity(
+ opacity: cryptoAmount.isEmpty ? 0 : 1,
+ child: Text(
+ "≈ ${cryptoAmount} ${cryptoCurrency.symbol}",
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.w600,
+ color: Theme.of(context).colorScheme.onSurfaceVariant),
+ ),
+ )
+ ],
+ ),
+ Padding(
+ padding: const EdgeInsets.all(18.0),
+ child: NewPrimaryButton(
+ onPressed: onContinuePressed,
+ text: S.of(context).continue_text,
+ color: Theme.of(context).colorScheme.primary,
+ textColor: Theme.of(context).colorScheme.onPrimary),
+ )
+ ],
+ );
+ }
+}
+
class BuySellDefaultAmountSelector extends StatelessWidget {
const BuySellDefaultAmountSelector(
{super.key,
@@ -100,6 +175,7 @@ class BuySellDefaultAmountSelector extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: GridView.builder(
shrinkWrap: true,
+ physics: NeverScrollableScrollPhysics(),
// +1 for "custom" option
itemCount: defaultAmounts.length + 1,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
diff --git a/lib/new-ui/pages/buy_sell/buy_sell_provider_page.dart b/lib/new-ui/pages/buy_sell/buy_sell_provider_page.dart
new file mode 100644
index 00000000..acbc7f71
--- /dev/null
+++ b/lib/new-ui/pages/buy_sell/buy_sell_provider_page.dart
@@ -0,0 +1,45 @@
+import 'package:cake_wallet/generated/i18n.dart';
+import 'package:cake_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart';
+import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
+import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart';
+import 'package:flutter/material.dart';
+
+class BuySellProviderPage extends StatelessWidget {
+ const BuySellProviderPage({super.key, required this.buySellViewModel});
+
+ final BuySellViewModel buySellViewModel;
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.vertical(top: Radius.circular(18)),
+ gradient: LinearGradient(
+ colors: [
+ Theme.of(context).colorScheme.surface,
+ Theme.of(context).colorScheme.surfaceDim,
+ ],
+ begin: Alignment.topCenter,
+ end: Alignment.bottomCenter,
+ ),
+ ),
+ child: SafeArea(
+ child: Column(
+ children: [
+ ModalTopBar(
+ title: _pageTitle,
+ leadingIcon: Icon(Icons.arrow_back_ios_new),
+ onLeadingPressed: Navigator.of(context).pop,
+ ),
+ Expanded(child: Column(
+
+ ))
+ ],
+ )),
+ );
+ }
+
+ String get _pageTitle => buySellViewModel.mode == BuySellPageMode.buy
+ ? S.current.buy
+ : S.current.sell + " " + (buySellViewModel.cryptoCurrency.fullName ?? "");
+}
diff --git a/lib/new-ui/widgets/floating_amount_input.dart b/lib/new-ui/widgets/floating_amount_input.dart
new file mode 100644
index 00000000..3367d0a3
--- /dev/null
+++ b/lib/new-ui/widgets/floating_amount_input.dart
@@ -0,0 +1,92 @@
+import 'package:cw_core/currency.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+class FloatingAmountInput extends StatefulWidget {
+ const FloatingAmountInput({super.key, required this.currency, required this.controller, this.focusNode, this.inputFormatters, this.onChanged, this.validator});
+
+ final Currency currency;
+ final TextEditingController controller;
+ final FocusNode? focusNode;
+ final List<TextInputFormatter>? inputFormatters;
+ final Function(String)? onChanged;
+ final FormFieldValidator<String>? validator;
+
+ @override
+ State<FloatingAmountInput> createState() => _FloatingAmountInputState();
+}
+
+class _FloatingAmountInputState extends State<FloatingAmountInput> {
+ bool _amountFocused = false;
+ late FocusNode focusNode = widget.focusNode ?? FocusNode();
+
+ @override
+ void initState() {
+ super.initState();
+ focusNode.addListener(() => setState(() => _amountFocused = focusNode.hasFocus));
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Center(
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.baseline,
+ textBaseline: TextBaseline.alphabetic,
+ children: [
+ IntrinsicWidth(
+ child: TextFormField(
+ controller: widget.controller,
+ focusNode: focusNode,
+ maxLines: 1,
+ onChanged: widget.onChanged,
+ autovalidateMode: AutovalidateMode.always,
+ validator: widget.validator,
+ keyboardType: TextInputType.numberWithOptions(
+ signed: false,
+ decimal: true,
+ ),
+ inputFormatters: <TextInputFormatter>[
+ FilteringTextInputFormatter.allow(
+ RegExp(r'^\d*[.,]?\d*$'),
+ ),
+ ],
+ decoration: InputDecoration(
+ isDense: true,
+ isCollapsed: true,
+ contentPadding: EdgeInsets.zero,
+ fillColor: Colors.transparent,
+ hoverColor: Colors.transparent,
+ focusedBorder: InputBorder.none,
+ enabledBorder: InputBorder.none,
+ hintText: _amountFocused || widget.controller.text.isNotEmpty
+ ? null
+ : "0.00",
+ hintStyle: Theme.of(context).textTheme.displayMedium?.copyWith(
+ fontWeight: FontWeight.w400,
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ),
+ style: Theme.of(context).textTheme.displayMedium?.copyWith(
+ fontWeight: FontWeight.w400,
+ fontSize: 45,
+ color: Theme.of(context).colorScheme.onSurface,
+ ),
+ ),
+ ),
+ const SizedBox(width: 8),
+ Text(
+ widget.currency.symbol,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: Theme.of(context).textTheme.displayMedium?.copyWith(
+ fontWeight: FontWeight.w400,
+ fontSize: 45,
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/src/screens/buy/buy_sell_page.dart b/lib/src/screens/buy/buy_sell_page.dart
index 73c238b5..9c05309c 100644
--- a/lib/src/screens/buy/buy_sell_page.dart
+++ b/lib/src/screens/buy/buy_sell_page.dart
@@ -3,6 +3,7 @@ import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/generated/i18n.dart';
+import 'package:cake_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart';
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart';
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart';
import 'package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart';
@@ -412,7 +413,7 @@ class BuySellPage extends BasePage {
borderColor: Theme.of(context).colorScheme.outlineVariant,
onPushPasteButton: (context) async {},
onPushAddressBookButton: (context) async {},
- fillColor: buySellViewModel.isBuyAction
+ fillColor: buySellViewModel.mode == BuySellPageMode.buy
? Theme.of(context).colorScheme.surfaceContainer
: Theme.of(context).colorScheme.surfaceContainerLow,
),
@@ -451,7 +452,7 @@ class BuySellPage extends BasePage {
addressTextFieldValidator: AddressValidator(type: buySellViewModel.cryptoCurrency),
onPushPasteButton: (context) async {},
onPushAddressBookButton: (context) async {},
- fillColor: buySellViewModel.isBuyAction
+ fillColor: buySellViewModel.mode == BuySellPageMode.buy
? Theme.of(context).colorScheme.surfaceContainerLow
: Theme.of(context).colorScheme.surfaceContainer,
useSatoshis: buySellViewModel.useSatoshi,
@@ -461,50 +462,14 @@ class BuySellPage extends BasePage {
if (responsiveLayoutUtil.shouldRenderMobileUI) {
return Observer(
builder: (_) {
- if (buySellViewModel.isBuyAction) {
- return MobileExchangeCardsSection(
- firstExchangeCard: fiatExchangeCard,
- secondExchangeCard: cryptoExchangeCard,
- onBuyTap: () => null,
- onSellTap: () =>
- buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
- isBuySellOption: true,
- );
- } else {
- return MobileExchangeCardsSection(
- firstExchangeCard: cryptoExchangeCard,
- secondExchangeCard: fiatExchangeCard,
- onBuyTap: () =>
- !buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
- onSellTap: () => null,
- isBuySellOption: true,
- );
- }
+ return Placeholder();
},
);
}
return Observer(
builder: (_) {
- if (buySellViewModel.isBuyAction) {
- return DesktopExchangeCardsSection(
- firstExchangeCard: fiatExchangeCard,
- secondExchangeCard: cryptoExchangeCard,
- onBuyTap: () => null,
- onSellTap: () =>
- buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
- isBuySellOption: true,
- );
- } else {
- return DesktopExchangeCardsSection(
- firstExchangeCard: cryptoExchangeCard,
- secondExchangeCard: fiatExchangeCard,
- onBuyTap: () =>
- !buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
- onSellTap: () => null,
- isBuySellOption: true,
- );
- }
+ return Placeholder();
},
);
}
diff --git a/lib/view_model/buy/buy_sell_view_model.dart b/lib/view_model/buy/buy_sell_view_model.dart
index 4d181d7f..100f1332 100644
--- a/lib/view_model/buy/buy_sell_view_model.dart
+++ b/lib/view_model/buy/buy_sell_view_model.dart
@@ -11,10 +11,13 @@ import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/provider_types.dart';
import 'package:cake_wallet/generated/i18n.dart';
+import 'package:cake_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart';
import 'package:cake_wallet/routes.dart';
+import 'package:cake_wallet/src/screens/buy/buy_sell_page.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/crypto_currency.dart';
+import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/cupertino.dart';
import 'package:mobx/mobx.dart';
@@ -25,6 +28,7 @@ class BuySellViewModel = BuySellViewModelBase with _$BuySellViewModel;
abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with Store {
BuySellViewModelBase(
AppStore appStore,
+ {required this.mode}
) : _cryptoAmount = '',
fiatAmount = '',
cryptoCurrencyAddress = '',
@@ -81,7 +85,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
final formattedFiatAmount = double.tryParse(fiatAmount);
final formattedCryptoAmount = double.tryParse(_cryptoAmount);
- return isBuyAction
+ return mode == BuySellPageMode.buy
? formattedFiatAmount ?? 200.0
: formattedCryptoAmount ?? (cryptoCurrency == CryptoCurrency.btc ? 0.001 : 1);
}
@@ -100,8 +104,8 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
@observable
List<FiatCurrency> fiatCurrencies;
- @observable
- bool isBuyAction = true;
+ final BuySellPageMode mode;
+
@observable
List<BuyProvider> providerList;
@@ -234,11 +238,6 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
_initialize();
}
- @action
- void changeBuySellAction() {
- isBuyAction = !isBuyAction;
- _initialize();
- }
@action
void changeFiatCurrency({required FiatCurrency currency}) {
@@ -267,16 +266,18 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
}
if (!isReadyToTrade && !isBuySellQuoteFailed) {
- _cryptoAmount = S.current.fetching;
+ _cryptoAmount = "...";
return;
} else if (isBuySellQuoteFailed) {
_cryptoAmount = '';
return;
}
+ printV(bestRateQuote);
if (bestRateQuote != null) {
final enteredAmount = double.tryParse(fiatAmount.replaceAll(',', '.')) ?? 0;
final amount = enteredAmount / bestRateQuote!.rate;
+ printV(amount);
_cryptoAmount = amount.toString().withMaxDecimals(cryptoCurrency.decimals);
} else {
@@ -391,7 +392,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
}
void _setProviders() =>
- providerList = isBuyAction ? availableBuyProviders : availableSellProviders;
+ providerList = mode == BuySellPageMode.buy ? availableBuyProviders : availableSellProviders;
Future<void> _initialize() async {
_setProviders();
@@ -420,7 +421,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
paymentMethodState = PaymentMethodLoading();
selectedPaymentMethod = null;
final result = await Future.wait(providerList.map((element) =>
- element.getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency, isBuyAction).timeout(
+ element.getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency, mode == BuySellPageMode.buy).timeout(
Duration(seconds: 10),
onTimeout: () => [],
)));
@@ -456,7 +457,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
buySellQuotState = BuySellQuotLoading();
final List<BuyProvider> validProviders = providerList.where((provider) {
- if (isBuyAction) {
+ if (mode == BuySellPageMode.buy) {
return provider.supportedCryptoList
.any((pair) => pair.from == cryptoCurrency && pair.to == fiatCurrency);
} else {
@@ -476,7 +477,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
fiatCurrency: fiatCurrency,
amount: amount,
paymentType: selectedPaymentMethod?.paymentMethodType,
- isBuyAction: isBuyAction,
+ isBuyAction: mode == BuySellPageMode.buy,
walletAddress: wallet.walletAddresses.address,
customPaymentMethodType: selectedPaymentMethod?.customPaymentMethodType,
)
@@ -498,7 +499,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
return;
}
- if (isBuyAction) {
+ if (mode == BuySellPageMode.buy) {
validQuotes.sort((a, b) => b.payout.compareTo(a.payout));
} else {
validQuotes.sort((a, b) => a.payout.compareTo(b.payout));
@@ -541,7 +542,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
final Quote effectiveBestRateQuote =
sortedRecommendedQuotes.reduce((a, b) {
- return isBuyAction ? a.rate < b.rate ? a : b : a.rate > b.rate ? a : b;
+ return mode == BuySellPageMode.buy ? a.rate < b.rate ? a : b : a.rate > b.rate ? a : b;
});
@@ -563,7 +564,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
context: context,
quote: selectedQuote!,
amount: amount,
- isBuyAction: isBuyAction,
+ isBuyAction: mode == BuySellPageMode.buy,
cryptoCurrencyAddress: cryptoCurrencyAddress,
);
} catch (e) {
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.