feat: Swap flow for Pay Anything. (#2489)

* feat: Swap flow for Pay Anything. [WIP] * feat: Pay Anything Swap Flow [WIP] * feat: Pay anything swap flow [WIP] - Add fiat values for send and receive amounts to swap details confirmation bottomsheet * feat: Pay anything swap flow [WIP] - Switch exchangeviewmodel back to factory - Expand address box to fill width * Handle second swipe to swap cta * Add fiat for receive amount * Scope Observers for bottomsheets and add validator * fix: Repeated instances issue for exchangeVM * fix: Estimated fee and balance not correctly updating after wallet switching occurs * fix: Available balance not updating properly after wallet switching * Fix: Give wallet some time to open before populating send info --------- Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>

David Adegoke committed Sep 6, 2025 at 17:16 UTC d1b62f2664ba4b3c199855fb4448ce5a0fb966bf
13 files changed +1282 -98
lib/di.dart
+21
@@ -43,6 +43,9 @@ import 'package:cake_wallet/src/screens/start_tor/start_tor_page.dart';
43 import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
44 import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
45 import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
46 +import 'package:cake_wallet/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart';
47 +import 'package:cake_wallet/src/widgets/bottom_sheet/swap_details_bottom_sheet.dart';
48 +
49 import 'package:cake_wallet/store/dashboard/order_filter_store.dart';
50 import 'package:cake_wallet/themes/core/theme_store.dart';
51 import 'package:cake_wallet/view_model/dev/monero_background_sync.dart';
@@ -530,6 +533,7 @@ Future<void> setup({
533 getIt.get<UnspentCoinsListViewModel>(),
534 getIt.get<FeesViewModel>(),
535 _walletInfoSource,
536 + getIt.get<FiatConversionStore>(),
537 ),
538 );
539
@@ -1113,6 +1117,7 @@ Future<void> setup({
1117 tradesStore: getIt.get<TradesStore>(),
1118 sendViewModel: getIt.get<SendViewModel>(),
1119 feesViewModel: getIt.get<FeesViewModel>(),
1120 + fiatConversionStore: getIt.get<FiatConversionStore>(),
1121 ),
1122 );
1123
@@ -1133,6 +1138,22 @@ Future<void> setup({
1138
1139 getIt.registerFactory(() => ExchangeTemplatePage(getIt.get<ExchangeViewModel>()));
1140
1141 + getIt.registerFactoryParam<SwapConfirmationBottomSheet, PaymentFlowResult, void>(
1142 + (paymentFlowResult, _) => SwapConfirmationBottomSheet(
1143 + paymentFlowResult: paymentFlowResult,
1144 + currentTheme: getIt.get<AppStore>().themeStore.currentTheme,
1145 + exchangeViewModel: getIt.get<ExchangeViewModel>(),
1146 + authService: getIt.get<AuthService>(),
1147 + ),
1148 + );
1149 +
1150 + getIt.registerFactory<SwapDetailsBottomSheet>(
1151 + () => SwapDetailsBottomSheet(
1152 + currentTheme: getIt.get<AppStore>().themeStore.currentTheme,
1153 + exchangeTradeViewModel: getIt.get<ExchangeTradeViewModel>(),
1154 + ),
1155 + );
1156 +
1157 getIt.registerFactory(() => PaymentViewModel(
1158 appStore: getIt.get<AppStore>(),
1159 walletInfoSource: _walletInfoSource,
lib/src/screens/exchange/exchange_page.dart
+17 -8
@@ -10,6 +10,7 @@ import 'package:cake_wallet/utils/debounce.dart';
10 import 'package:cake_wallet/utils/payment_request.dart';
11 import 'package:cake_wallet/utils/responsive_layout_util.dart';
12 import 'package:cw_core/sync_status.dart';
13 +import 'package:cw_core/utils/print_verbose.dart';
14 import 'package:cw_core/wallet_type.dart';
15 import 'package:cake_wallet/entities/parse_address_from_domain.dart';
16 import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
@@ -129,8 +130,7 @@ class ExchangePage extends BasePage {
130 color: Theme.of(context).colorScheme.primary,
131 size: 16,
132 );
132 - final _closeButton =
133 - currentTheme.isDark ? closeButtonImageDarkTheme : closeButtonImage;
133 + final _closeButton = currentTheme.isDark ? closeButtonImageDarkTheme : closeButtonImage;
134
135 bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI;
136
@@ -368,10 +368,13 @@ class ExchangePage extends BasePage {
368
369 void applyTemplate(
370 BuildContext context, ExchangeViewModel exchangeViewModel, ExchangeTemplate template) async {
371 - final depositCryptoCurrency = CryptoCurrency.safeParseCurrencyFromString(template.depositCurrency);
372 - final receiveCryptoCurrency = CryptoCurrency.safeParseCurrencyFromString(template.receiveCurrency);
371 + final depositCryptoCurrency =
372 + CryptoCurrency.safeParseCurrencyFromString(template.depositCurrency);
373 + final receiveCryptoCurrency =
374 + CryptoCurrency.safeParseCurrencyFromString(template.receiveCurrency);
375
374 - if (depositCryptoCurrency == null || receiveCryptoCurrency == null) { ///TO DO: add support for user tokens
376 + if (depositCryptoCurrency == null || receiveCryptoCurrency == null) {
377 + ///TO DO: add support for user tokens
378 return;
379 }
380
@@ -610,9 +613,15 @@ class ExchangePage extends BasePage {
613 });
614
615 if (initialPaymentRequest != null) {
613 - exchangeViewModel.receiveCurrency = CryptoCurrency.fromString(initialPaymentRequest!.scheme);
614 - exchangeViewModel.depositAmount = initialPaymentRequest!.amount;
615 - exchangeViewModel.receiveAddress = initialPaymentRequest!.address;
616 + try {
617 + exchangeViewModel.receiveCurrency =
618 + CryptoCurrency.fromString(initialPaymentRequest!.scheme);
619 + exchangeViewModel.receiveAmount = initialPaymentRequest!.amount;
620 + exchangeViewModel.receiveAddress = initialPaymentRequest!.address;
621 + } catch (e) {
622 + printV('error: ${e.toString()}');
623 + // TODO
624 + }
625 }
626
627 _isReactionsSet = true;
lib/src/screens/send/widgets/send_card.dart
+42 -17
@@ -1,17 +1,22 @@
1 +import 'dart:async';
2 +
3 import 'package:cake_wallet/core/open_crypto_pay/open_cryptopay_service.dart';
4 import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
5 import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dart';
4 -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
6 +import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
7 import 'package:cake_wallet/src/widgets/bottom_sheet/payment_confirmation_bottom_sheet.dart';
8 import 'package:cake_wallet/src/widgets/bottom_sheet/wallet_switcher_bottom_sheet.dart';
9 +import 'package:cake_wallet/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart';
10 import 'package:cake_wallet/src/widgets/picker.dart';
11 import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
12 import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
13 +
14 import 'package:cake_wallet/themes/core/material_base_theme.dart';
15 import 'package:cake_wallet/utils/payment_request.dart';
16 import 'package:cake_wallet/utils/responsive_layout_util.dart';
17 import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
18 import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart';
19 +import 'package:cake_wallet/exchange/trade.dart';
20 import 'package:cw_core/crypto_currency.dart';
21 import 'package:cw_core/currency.dart';
22 import 'package:cake_wallet/routes.dart';
@@ -29,6 +34,7 @@ import 'package:cake_wallet/utils/show_pop_up.dart';
34 import 'package:cake_wallet/src/widgets/address_text_field.dart';
35 import 'package:cake_wallet/generated/i18n.dart';
36 import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
37 +import 'package:cake_wallet/di.dart';
38
39 class SendCard extends StatefulWidget {
40 SendCard({
@@ -100,6 +106,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
106 final FocusNode addressFocusNode;
107
108 bool _effectsInstalled = false;
109 + BuildContext? loadingBottomSheetContext;
110
111 @override
112 void initState() {
@@ -114,18 +121,9 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
121 WidgetsBinding.instance.addPostFrameCallback(
122 (timeStamp) {
123 if (mounted) {
117 - // TODO: revert this as well
118 - showPopUp<void>(
119 - context: context,
120 - builder: (BuildContext context) {
121 - return AlertWithOneAction(
122 - alertTitle: S.of(context).error,
123 - alertContent: S.of(context).unmatched_currencies,
124 - buttonText: S.of(context).ok,
125 - buttonAction: () => Navigator.of(context).pop(),
126 - );
127 - },
128 - );
124 + final separator = initialPaymentRequest!.scheme.isNotEmpty ? ":" : "";
125 + final uri = initialPaymentRequest!.scheme + separator + initialPaymentRequest!.address;
126 + _handlePaymentFlow(uri, initialPaymentRequest!);
127 }
128 },
129 );
@@ -145,9 +143,6 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
143
144 Future<void> _handlePaymentFlow(String uri, PaymentRequest paymentRequest) async {
145 try {
148 - // TODO: just remove this line
149 - throw "Pay Anything is temporarily disabled";
150 -
146 final result = await paymentViewModel.processAddress(uri);
147
148 switch (result.type) {
@@ -201,7 +196,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
196 paymentRequest,
197 result,
198 ),
204 - onSwap: () => Navigator.of(context).pushNamed(Routes.exchange, arguments: paymentRequest),
199 + onSwap: () => _handleSwapFlow(paymentViewModel, result),
200 );
201 },
202 );
@@ -246,6 +241,25 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
241 walletSwitcherViewModel.selectWallet(result.wallet!);
242 final success = await walletSwitcherViewModel.switchToSelectedWallet();
243 if (success) {
244 + WidgetsBinding.instance.addPostFrameCallback((_) {
245 + if (context.mounted) {
246 + showModalBottomSheet<void>(
247 + context: context,
248 + isDismissible: false,
249 + builder: (BuildContext context) {
250 + loadingBottomSheetContext = context;
251 + return LoadingBottomSheet(
252 + titleText: S.of(context).loading_your_wallet,
253 + );
254 + },
255 + );
256 + }
257 + });
258 + await Future.delayed(const Duration(seconds: 2));
259 + if (loadingBottomSheetContext != null &&
260 + loadingBottomSheetContext!.mounted) {
261 + Navigator.of(loadingBottomSheetContext!).pop();
262 + }
263 _applyPaymentRequest(paymentRequest);
264 }
265 }
@@ -261,6 +275,17 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
275 noteController.text = paymentRequest.note;
276 }
277
278 + Future<void> _handleSwapFlow(PaymentViewModel paymentViewModel, PaymentFlowResult result) async {
279 + Navigator.of(context).pop();
280 + final bottomSheet = getIt.get<SwapConfirmationBottomSheet>(param1: result);
281 + await showModalBottomSheet<Trade?>(
282 + context: context,
283 + isDismissible: true,
284 + isScrollControlled: true,
285 + builder: (BuildContext context) => bottomSheet,
286 + );
287 + }
288 +
289 @override
290 Widget build(BuildContext context) {
291 super.build(context);
lib/src/widgets/base_text_form_field.dart
+1 -1
@@ -124,7 +124,7 @@ class BaseTextFormField extends StatelessWidget {
124 isDense: isDense,
125 alignLabelWithHint: alignLabelWithHint,
126 contentPadding: contentPadding,
127 - floatingLabelBehavior: FloatingLabelBehavior.never,
127 + floatingLabelBehavior: floatingLabelBehavior ?? FloatingLabelBehavior.never,
128 prefixIconConstraints: prefixIconConstraints ??
129 const BoxConstraints(minWidth: 0, minHeight: 0),
130 prefix: prefix,
lib/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart new
+554
@@ -0,0 +1,554 @@
1 +import 'dart:developer';
2 +
3 +import 'package:cake_wallet/core/amount_validator.dart';
4 +import 'package:cake_wallet/core/auth_service.dart';
5 +import 'package:cake_wallet/di.dart';
6 +import 'package:cake_wallet/exchange/limits_state.dart';
7 +import 'package:cake_wallet/generated/i18n.dart';
8 +import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart';
9 +import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
10 +import 'package:cake_wallet/src/widgets/bottom_sheet/swap_details_bottom_sheet.dart';
11 +import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
12 +import 'package:cake_wallet/utils/address_formatter.dart';
13 +import 'package:cake_wallet/utils/debounce.dart';
14 +
15 +import 'package:flutter/material.dart';
16 +import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
17 +import 'package:cake_wallet/src/widgets/primary_button.dart';
18 +import 'package:cw_core/wallet_type.dart';
19 +import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
20 +import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart';
21 +import 'package:cake_wallet/exchange/exchange_trade_state.dart';
22 +import 'package:cake_wallet/themes/core/material_base_theme.dart';
23 +import 'package:flutter_mobx/flutter_mobx.dart';
24 +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
25 +import 'package:cake_wallet/utils/show_pop_up.dart';
26 +import 'package:mobx/mobx.dart';
27 +
28 +class SwapConfirmationBottomSheet extends BaseBottomSheet {
29 + SwapConfirmationBottomSheet({
30 + Key? key,
31 + required this.paymentFlowResult,
32 + required this.currentTheme,
33 + required this.exchangeViewModel,
34 + required this.authService,
35 + this.sessionId,
36 + }) : super(
37 + titleText: S.current.swap,
38 + footerType: FooterType.none,
39 + maxHeight: 900,
40 + currentTheme: currentTheme,
41 + );
42 +
43 + final PaymentFlowResult paymentFlowResult;
44 + final MaterialThemeBase currentTheme;
45 + final ExchangeViewModel exchangeViewModel;
46 + final AuthService authService;
47 + final String? sessionId;
48 + @override
49 + Widget contentWidget(BuildContext context) {
50 + return SingleChildScrollView(
51 + padding: EdgeInsets.only(
52 + bottom: MediaQuery.of(context).viewInsets.bottom,
53 + ),
54 + child: SwapConfirmationContent(
55 + paymentFlowResult: paymentFlowResult,
56 + exchangeViewModel: exchangeViewModel,
57 + authService: authService,
58 + ),
59 + );
60 + }
61 +}
62 +
63 +class SwapConfirmationContent extends StatefulWidget {
64 + const SwapConfirmationContent({
65 + Key? key,
66 + required this.paymentFlowResult,
67 + required this.exchangeViewModel,
68 + required this.authService,
69 + }) : super(key: key);
70 +
71 + final PaymentFlowResult paymentFlowResult;
72 + final ExchangeViewModel exchangeViewModel;
73 + final AuthService authService;
74 +
75 + @override
76 + SwapConfirmationContentState createState() => SwapConfirmationContentState();
77 +}
78 +
79 +class SwapConfirmationContentState extends State<SwapConfirmationContent> {
80 + late TextEditingController _amountController;
81 + late TextEditingController _amountFiatController;
82 + late TextEditingController _addressController;
83 + late TextEditingController _noteController;
84 +
85 + final _receiveAmountDebounce = Debounce(Duration(milliseconds: 500));
86 + final _receiveAmountFiatDebounce = Debounce(Duration(milliseconds: 500));
87 + final FocusNode _amountFocus = FocusNode();
88 + final FocusNode _amountFiatFocus = FocusNode();
89 + final FocusNode _addressFocus = FocusNode();
90 + final FocusNode _noteFocus = FocusNode();
91 + final _formKey = GlobalKey<FormState>();
92 +
93 + ReactionDisposer? _receiveAmountReaction;
94 + ReactionDisposer? _receiveAddressReaction;
95 + ReactionDisposer? _tradeStateReaction;
96 + ReactionDisposer? _bestRateReaction;
97 + ReactionDisposer? _receiveAmountFiatReaction;
98 +
99 + bool _showingFailureDialog = false;
100 + bool _showingSwapDetailsDialog = false;
101 +
102 + @override
103 + void initState() {
104 + super.initState();
105 + _addressController =
106 + TextEditingController(text: widget.paymentFlowResult.addressDetectionResult?.address ?? '');
107 + _amountController = TextEditingController(
108 + text: widget.paymentFlowResult.addressDetectionResult?.amount?.isNotEmpty ?? false
109 + ? widget.paymentFlowResult.addressDetectionResult?.amount
110 + : '0.00');
111 + _amountFiatController =
112 + TextEditingController(text: widget.exchangeViewModel.receiveAmountFiatFormatted);
113 + _noteController =
114 + TextEditingController(text: widget.paymentFlowResult.addressDetectionResult?.note ?? '');
115 +
116 + WidgetsBinding.instance.addPostFrameCallback(
117 + (_) => _setUpReactions(
118 + context,
119 + widget.exchangeViewModel,
120 + widget.paymentFlowResult,
121 + ),
122 + );
123 + }
124 +
125 + @override
126 + void dispose() {
127 + _amountController.dispose();
128 + _amountFiatController.dispose();
129 + _addressController.dispose();
130 + _noteController.dispose();
131 + _amountFocus.dispose();
132 + _amountFiatFocus.dispose();
133 + _addressFocus.dispose();
134 + _noteFocus.dispose();
135 + _receiveAmountReaction?.call();
136 + _receiveAddressReaction?.call();
137 + _tradeStateReaction?.call();
138 + _bestRateReaction?.call();
139 + _receiveAmountFiatReaction?.call();
140 + _showingFailureDialog = false;
141 + _showingSwapDetailsDialog = false;
142 + widget.exchangeViewModel.bestRateSync.cancel();
143 + super.dispose();
144 + }
145 +
146 + @override
147 + Widget build(BuildContext context) {
148 + final detectedCurrencyName = walletTypeToCryptoCurrency(widget.paymentFlowResult.walletType!);
149 +
150 + return Form(
151 + key: _formKey,
152 + child: Padding(
153 + padding: const EdgeInsets.symmetric(horizontal: 16),
154 + child: Column(
155 + mainAxisSize: MainAxisSize.min,
156 + crossAxisAlignment: CrossAxisAlignment.start,
157 + children: [
158 + Row(
159 + mainAxisAlignment: MainAxisAlignment.center,
160 + children: [
161 + CakeImageWidget(
162 + imageUrl: widget.exchangeViewModel.depositCurrency.iconPath!,
163 + width: 32,
164 + height: 32,
165 + ),
166 + const SizedBox(width: 12),
167 + Icon(Icons.arrow_forward, size: 24),
168 + const SizedBox(width: 12),
169 + CakeImageWidget(
170 + imageUrl:
171 + walletTypeToCryptoCurrency(widget.paymentFlowResult.walletType!).iconPath!,
172 + width: 32,
173 + height: 32,
174 + ),
175 + ],
176 + ),
177 + const SizedBox(height: 16),
178 + SwapConfirmationTextfield(
179 + key: ValueKey('swap_confirmation_bottomsheet_amount_textfield_key'),
180 + hintText: 'Amount (${detectedCurrencyName})',
181 + focusNode: _amountFocus,
182 + controller: _amountController,
183 + validator: (value) {
184 + return AmountValidator(
185 + isAutovalidate: true,
186 + currency: widget.exchangeViewModel.receiveCurrency,
187 + minValue: widget.exchangeViewModel.limits.min.toString(),
188 + maxValue: widget.exchangeViewModel.limits.max.toString(),
189 + ).call(value);
190 + },
191 + ),
192 + Observer(
193 + builder: (_) {
194 + String? min = '0.0';
195 + String? max = '0.0';
196 +
197 + final limitsState = widget.exchangeViewModel.limitsState;
198 + if (limitsState is LimitsLoadedSuccessfully) {
199 + min = limitsState.limits.min?.toString();
200 + max = limitsState.limits.max?.toString();
201 + }
202 +
203 + if (limitsState is LimitsLoadedFailure) {
204 + min = '0.0';
205 + max = '0.0';
206 + }
207 +
208 + if (limitsState is LimitsIsLoading) {
209 + min = '...';
210 + max = '...';
211 + }
212 + if (min != null || max != null) {
213 + return Container(
214 + height: 15,
215 + child: Row(
216 + mainAxisAlignment: MainAxisAlignment.start,
217 + children: <Widget>[
218 + min != null
219 + ? Text(
220 + key: ValueKey('min_limit_text_key'),
221 + S.of(context).min_value(min, detectedCurrencyName.toString()),
222 + style: Theme.of(context).textTheme.bodySmall!.copyWith(
223 + fontSize: 10,
224 + height: 1.2,
225 + color: Theme.of(context).colorScheme.onSurfaceVariant,
226 + ),
227 + )
228 + : Offstage(),
229 + min != null ? SizedBox(width: 10) : Offstage(),
230 + max != null
231 + ? Text(
232 + key: ValueKey('max_limit_text_key'),
233 + S.of(context).max_value(max, detectedCurrencyName.toString()),
234 + style: Theme.of(context).textTheme.bodySmall!.copyWith(
235 + fontSize: 10,
236 + height: 1.2,
237 + color: Theme.of(context).colorScheme.onSurfaceVariant,
238 + ),
239 + )
240 + : Offstage(),
241 + ],
242 + ),
243 + );
244 + }
245 +
246 + return SizedBox.shrink();
247 + },
248 + ),
249 + const SizedBox(height: 8),
250 + SwapConfirmationTextfield(
251 + key: ValueKey('swap_confirmation_bottomsheet_amount_fiat_textfield_key'),
252 + hintText: 'Amount (${widget.exchangeViewModel.fiat.title})',
253 + focusNode: _amountFiatFocus,
254 + controller: _amountFiatController,
255 + ),
256 + const SizedBox(height: 8),
257 + SwapConfirmationTextfield(
258 + key: ValueKey('swap_confirmation_bottomsheet_address_textfield_key'),
259 + isAddress: true,
260 + walletType: cryptoCurrencyToWalletType(widget.exchangeViewModel.receiveCurrency),
261 + hintText: 'Destination Address',
262 + focusNode: _addressFocus,
263 + controller: _addressController,
264 + ),
265 + const SizedBox(height: 8),
266 + SwapConfirmationTextfield(
267 + maxLines: 1,
268 + key: ValueKey('swap_confirmation_bottomsheet_note_textfield_key'),
269 + hintText: 'Transaction Note',
270 + focusNode: _noteFocus,
271 + controller: _noteController,
272 + ),
273 + SizedBox(height: 8),
274 + Center(
275 + child: Text(
276 + 'Tap field to edit values',
277 + style: Theme.of(context).textTheme.bodySmall!.copyWith(
278 + fontSize: 10,
279 + color: Theme.of(context).colorScheme.onSurfaceVariant,
280 + ),
281 + ),
282 + ),
283 + SizedBox(height: 32),
284 + SwapConfirmationFooter(
285 + exchangeViewModel: widget.exchangeViewModel,
286 + formKey: _formKey,
287 + authService: widget.authService,
288 + ),
289 + ],
290 + ),
291 + ),
292 + );
293 + }
294 +
295 + void _setUpReactions(
296 + BuildContext context,
297 + ExchangeViewModel exchangeViewModel,
298 + PaymentFlowResult paymentFlowResult,
299 + ) async {
300 + _receiveAmountReaction = reaction((_) => exchangeViewModel.receiveAmount, (String amount) {
301 + if (_amountController.text != amount) {
302 + _amountController.text = amount;
303 + }
304 + });
305 +
306 + _receiveAmountFiatReaction =
307 + reaction((_) => exchangeViewModel.receiveAmountFiatFormatted, (String amount) {
308 + if (_amountFiatController.text != amount) {
309 + _amountFiatController.text = amount;
310 + }
311 + });
312 +
313 + _receiveAddressReaction = reaction((_) => exchangeViewModel.receiveAddress, (String address) {
314 + if (_addressController.text != address) {
315 + _addressController.text = address;
316 + }
317 + });
318 +
319 + _tradeStateReaction = reaction((_) => exchangeViewModel.tradeState, (ExchangeTradeState state) {
320 + if (state is TradeIsCreatedFailure && !_showingFailureDialog) {
321 + _showingFailureDialog = true;
322 + WidgetsBinding.instance.addPostFrameCallback((_) {
323 + if (context.mounted) {
324 + showPopUp<void>(
325 + context: context,
326 + builder: (BuildContext context) {
327 + return AlertWithOneAction(
328 + key: const ValueKey('swap_confirmation_trade_creation_failure_dialog_key'),
329 + buttonKey:
330 + const ValueKey('swap_confirmation_trade_creation_failure_dialog_button_key'),
331 + alertTitle: S.of(context).provider_error(state.title),
332 + alertContent: state.error,
333 + buttonText: S.of(context).ok,
334 + buttonAction: () {
335 + _showingFailureDialog = false;
336 + if (Navigator.of(context).canPop()) {
337 + Navigator.of(context).pop();
338 + }
339 + },
340 + );
341 + },
342 + );
343 + }
344 + });
345 + }
346 +
347 + if (state is TradeIsCreatedSuccessfully) {
348 + exchangeViewModel.reset();
349 + if (Navigator.of(context).canPop() && !_showingSwapDetailsDialog) {
350 + _showingSwapDetailsDialog = true;
351 + Navigator.of(context).pop();
352 + showModalBottomSheet<void>(
353 + context: context,
354 + isDismissible: true,
355 + isScrollControlled: true,
356 + builder: (BuildContext context) {
357 + _showingSwapDetailsDialog = false;
358 + return getIt.get<SwapDetailsBottomSheet>();
359 + },
360 + );
361 + }
362 + }
363 + });
364 +
365 + _bestRateReaction = reaction((_) => exchangeViewModel.bestRate, (double rate) {
366 + if (exchangeViewModel.isFixedRateMode) {
367 + exchangeViewModel.changeReceiveAmount(amount: _amountController.text);
368 + }
369 + });
370 +
371 + _addressController
372 + .addListener(() => exchangeViewModel.receiveAddress = _addressController.text);
373 +
374 + _amountController.addListener(() {
375 + if (_amountController.text != exchangeViewModel.receiveAmount) {
376 + _receiveAmountDebounce.run(() {
377 + exchangeViewModel.loadLimits();
378 + exchangeViewModel.changeReceiveAmount(amount: _amountController.text);
379 + exchangeViewModel.isReceiveAmountEntered = true;
380 + });
381 + }
382 + });
383 +
384 + _amountFiatController.addListener(() {
385 + if (_amountFiatController.text != exchangeViewModel.receiveAmountFiatFormatted) {
386 + _receiveAmountFiatDebounce.run(() {
387 + exchangeViewModel.loadLimits();
388 + exchangeViewModel.setReceiveAmountFromFiat(fiatAmount: _amountFiatController.text);
389 + });
390 + }
391 + });
392 +
393 + _amountFocus.addListener(() {
394 + if (_amountFocus.hasFocus) {
395 + exchangeViewModel.enableFixedRateMode();
396 + }
397 + });
398 +
399 + exchangeViewModel.receiveCurrency = walletTypeToCryptoCurrency(paymentFlowResult.walletType!);
400 + await exchangeViewModel.fetchFiatPrice(exchangeViewModel.receiveCurrency);
401 +
402 + exchangeViewModel.receiveAddress = _addressController.text;
403 + exchangeViewModel.depositAddress = exchangeViewModel.wallet.walletAddresses.addressForExchange;
404 + exchangeViewModel.receiveAmount = _amountController.text;
405 + _amountFiatController.text = exchangeViewModel.receiveAmountFiatFormatted;
406 + exchangeViewModel.isReceiveAmountEntered = true;
407 + exchangeViewModel.isFixedRateMode = true;
408 + }
409 +}
410 +
411 +class SwapConfirmationTextfield extends StatelessWidget {
412 + const SwapConfirmationTextfield({
413 + super.key,
414 + required this.focusNode,
415 + required this.controller,
416 + required this.hintText,
417 + this.walletType,
418 + this.isAddress = false,
419 + this.maxLines = 1,
420 + this.validator,
421 + });
422 +
423 + final FocusNode focusNode;
424 + final TextEditingController controller;
425 + final String hintText;
426 + final WalletType? walletType;
427 + final bool isAddress;
428 + final int maxLines;
429 + final String? Function(String?)? validator;
430 + @override
431 + Widget build(BuildContext context) {
432 + return Container(
433 + width: double.infinity,
434 + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
435 + decoration: ShapeDecoration(
436 + color: Theme.of(context).colorScheme.surfaceContainer,
437 + shape: RoundedRectangleBorder(
438 + borderRadius: BorderRadius.circular(10),
439 + side: focusNode.hasFocus
440 + ? BorderSide(color: Theme.of(context).colorScheme.primary)
441 + : BorderSide.none,
442 + ),
443 + ),
444 + child: Column(
445 + crossAxisAlignment: CrossAxisAlignment.start,
446 + children: [
447 + Text(
448 + hintText,
449 + style: Theme.of(context).textTheme.bodySmall!.copyWith(
450 + fontSize: 10,
451 + color: Theme.of(context).colorScheme.onSurfaceVariant,
452 + ),
453 + ),
454 + if (isAddress) SizedBox(height: 8),
455 + isAddress
456 + ? AddressFormatter.buildSegmentedAddress(
457 + address: controller.text,
458 + walletType: walletType,
459 + evenTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
460 + fontWeight: FontWeight.w400,
461 + fontSize: 12,
462 + ),
463 + )
464 + : BaseTextFormField(
465 + isDense: true,
466 + hintText: hintText,
467 + focusNode: focusNode,
468 + hasUnderlineBorder: true,
469 + borderWidth: 0.0,
470 + controller: controller,
471 + maxLines: maxLines,
472 + validator: validator,
473 + ),
474 + ],
475 + ),
476 + );
477 + }
478 +}
479 +
480 +class SwapConfirmationFooter extends StatelessWidget {
481 + const SwapConfirmationFooter({
482 + super.key,
483 + required this.exchangeViewModel,
484 + required this.formKey,
485 + required this.authService,
486 + });
487 +
488 + final ExchangeViewModel exchangeViewModel;
489 + final AuthService authService;
490 + final GlobalKey<FormState> formKey;
491 +
492 + @override
493 + Widget build(BuildContext context) {
494 + return Container(
495 + height: 150,
496 + width: double.infinity,
497 + padding: const EdgeInsets.symmetric(horizontal: 8),
498 + child: Observer(
499 + builder: (_) {
500 + final isLoading = exchangeViewModel.tradeState is TradeIsCreating ||
501 + exchangeViewModel.limitsState is LimitsIsLoading;
502 + final isDisabled = exchangeViewModel.selectedProviders.isEmpty ||
503 + exchangeViewModel.receiveAmount.isEmpty ||
504 + exchangeViewModel.receiveAddress.isEmpty;
505 +
506 + return Column(
507 + mainAxisSize: MainAxisSize.min,
508 + children: [
509 + PrimaryButton(
510 + text: S.current.cancel,
511 + onPressed: isLoading
512 + ? null
513 + : () {
514 + if (Navigator.of(context).canPop()) {
515 + Navigator.of(context).pop(null);
516 + }
517 + },
518 + color: Theme.of(context).colorScheme.surfaceContainer,
519 + textColor: Theme.of(context).colorScheme.onSecondaryContainer,
520 + ),
521 + const SizedBox(height: 12),
522 + LoadingPrimaryButton(
523 + text: S.current.continue_text,
524 + onPressed: exchangeViewModel.isAvailableInSelected
525 + ? () {
526 + FocusScope.of(context).unfocus();
527 +
528 + if (formKey.currentState != null && formKey.currentState!.validate()) {
529 + final check = exchangeViewModel.shouldDisplayTOTP();
530 + authService.authenticateAction(
531 + context,
532 + conditionToDetermineIfToUse2FA: check,
533 + onAuthSuccess: (value) {
534 + if (value) {
535 + exchangeViewModel.createTrade();
536 + }
537 + },
538 + );
539 + }
540 + }
541 + : () => PresentProviderPicker(exchangeViewModel: exchangeViewModel)
542 + .presentProviderPicker(context),
543 + color: Theme.of(context).colorScheme.primary,
544 + textColor: Theme.of(context).colorScheme.onPrimary,
545 + isDisabled: isDisabled,
546 + isLoading: isLoading,
547 + ),
548 + ],
549 + );
550 + },
551 + ),
552 + );
553 + }
554 +}
lib/src/widgets/bottom_sheet/swap_details_bottom_sheet.dart new
+441
@@ -0,0 +1,441 @@
1 +import 'package:cake_wallet/exchange/trade.dart';
2 +import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
3 +import 'package:cake_wallet/utils/address_formatter.dart';
4 +import 'package:cake_wallet/utils/show_bar.dart';
5 +import 'package:flutter/material.dart';
6 +import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
7 +import 'package:cw_core/wallet_type.dart';
8 +import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart';
9 +import 'package:cake_wallet/src/widgets/standard_slide_button_widget.dart';
10 +import 'package:cake_wallet/themes/core/material_base_theme.dart';
11 +import 'package:cake_wallet/core/execution_state.dart';
12 +import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
13 +import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
14 +import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
15 +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
16 +import 'package:cake_wallet/utils/show_pop_up.dart';
17 +import 'package:cake_wallet/generated/i18n.dart';
18 +import 'package:cw_core/utils/print_verbose.dart';
19 +import 'package:flutter/services.dart';
20 +import 'package:mobx/mobx.dart';
21 +import 'package:flutter_mobx/flutter_mobx.dart';
22 +import 'package:cake_wallet/reactions/wallet_connect.dart';
23 +
24 +class SwapDetailsBottomSheet extends StatefulWidget {
25 + SwapDetailsBottomSheet({
26 + Key? key,
27 + required this.currentTheme,
28 + required this.exchangeTradeViewModel,
29 + }) : super(key: key);
30 +
31 + final MaterialThemeBase currentTheme;
32 + final ExchangeTradeViewModel exchangeTradeViewModel;
33 +
34 + @override
35 + State<SwapDetailsBottomSheet> createState() => _SwapDetailsBottomSheetState();
36 +}
37 +
38 +class _SwapDetailsBottomSheetState extends State<SwapDetailsBottomSheet> {
39 + bool _effectsInstalled = false;
40 + ReactionDisposer? _exchangeStateReaction;
41 + BuildContext? _loadingBottomSheetContext;
42 +
43 + @override
44 + void initState() {
45 + super.initState();
46 + WidgetsBinding.instance.addPostFrameCallback((_) => _setEffects());
47 + }
48 +
49 + @override
50 + void dispose() {
51 + widget.exchangeTradeViewModel.timer?.cancel();
52 + _exchangeStateReaction?.reaction.dispose();
53 + super.dispose();
54 + }
55 +
56 + void _setEffects() {
57 + if (_effectsInstalled) return;
58 +
59 + _exchangeStateReaction = reaction(
60 + (_) => widget.exchangeTradeViewModel.sendViewModel.state,
61 + (ExecutionState state) async {
62 + if (state is! IsExecutingState &&
63 + state is! TransactionCommitting &&
64 + _loadingBottomSheetContext != null &&
65 + _loadingBottomSheetContext!.mounted) {
66 + Navigator.of(_loadingBottomSheetContext!).pop();
67 + }
68 +
69 + if (state is FailureState) {
70 + WidgetsBinding.instance.addPostFrameCallback((_) {
71 + showPopUp<void>(
72 + context: context,
73 + builder: (BuildContext popupContext) {
74 + return AlertWithOneAction(
75 + key: ValueKey('swap_details_send_failure_dialog_key'),
76 + buttonKey: ValueKey('swap_details_send_failure_dialog_button_key'),
77 + alertTitle: S.of(popupContext).error,
78 + alertContent: state.error,
79 + buttonText: S.of(popupContext).ok,
80 + buttonAction: () => Navigator.of(popupContext).pop(),
81 + );
82 + },
83 + );
84 + });
85 + }
86 +
87 + if (state is IsExecutingState) {
88 + // Wait a bit to avoid showing the loading dialog if transaction is failed
89 + await Future.delayed(const Duration(milliseconds: 300));
90 + final currentState = widget.exchangeTradeViewModel.sendViewModel.state;
91 + if (currentState is ExecutedSuccessfullyState || currentState is FailureState) {
92 + return;
93 + }
94 +
95 + WidgetsBinding.instance.addPostFrameCallback((_) {
96 + if (context.mounted) {
97 + showModalBottomSheet<void>(
98 + context: context,
99 + isDismissible: false,
100 + builder: (BuildContext context) {
101 + _loadingBottomSheetContext = context;
102 + return LoadingBottomSheet(
103 + titleText: S.of(context).generating_transaction,
104 + );
105 + },
106 + );
107 + }
108 + });
109 + }
110 +
111 + if (state is ExecutedSuccessfullyState) {
112 + WidgetsBinding.instance.addPostFrameCallback((_) async {
113 + if (context.mounted) {
114 + await widget.exchangeTradeViewModel.sendViewModel.commitTransaction(context);
115 + }
116 + });
117 + }
118 +
119 + if (state is TransactionCommitted) {
120 + WidgetsBinding.instance.addPostFrameCallback(
121 + (_) async {
122 + if (!mounted) return;
123 +
124 + await showModalBottomSheet<void>(
125 + context: context,
126 + isScrollControlled: true,
127 + builder: (BuildContext bottomSheetContext) {
128 + return InfoBottomSheet(
129 + currentTheme: widget.currentTheme,
130 + footerType: FooterType.singleActionButton,
131 + titleText: S.of(bottomSheetContext).transaction_sent,
132 + contentImage: 'assets/images/birthday_cake.png',
133 + singleActionButtonText: S.of(bottomSheetContext).close,
134 + singleActionButtonKey: ValueKey('swap_details_sent_dialog_ok_button_key'),
135 + onSingleActionButtonPressed: () {
136 + Navigator.of(bottomSheetContext).pop();
137 + if (mounted) {
138 + Navigator.of(context).pop();
139 + }
140 + ;
141 + },
142 + );
143 + },
144 + );
145 + },
146 + );
147 + }
148 + },
149 + );
150 +
151 + _effectsInstalled = true;
152 + }
153 +
154 + @override
155 + Widget build(BuildContext context) {
156 + return _SwapDetailsBottomSheetContent(
157 + titleText: 'Confirm Swap',
158 + footerType: FooterType.none,
159 + maxHeight: 900,
160 + currentTheme: widget.currentTheme,
161 + exchangeTradeViewModel: widget.exchangeTradeViewModel,
162 + onExecuteSwap: _executeSwap,
163 + );
164 + }
165 +
166 + void _executeSwap() async {
167 + if (!widget.exchangeTradeViewModel.isSendable) return;
168 +
169 + try {
170 + await widget.exchangeTradeViewModel.confirmSending();
171 + } catch (e) {
172 + printV('Error executing swap: $e');
173 + if (mounted) {
174 + showPopUp<void>(
175 + context: context,
176 + builder: (BuildContext popupContext) {
177 + return AlertWithOneAction(
178 + key: ValueKey('swap_details_execution_error_dialog_key'),
179 + buttonKey: ValueKey('swap_details_execution_error_dialog_button_key'),
180 + alertTitle: S.of(popupContext).error,
181 + alertContent: e.toString(),
182 + buttonText: S.of(popupContext).ok,
183 + buttonAction: () => Navigator.of(popupContext).pop(),
184 + );
185 + },
186 + );
187 + }
188 + }
189 + }
190 +}
191 +
192 +class _SwapDetailsBottomSheetContent extends BaseBottomSheet {
193 + _SwapDetailsBottomSheetContent({
194 + required String titleText,
195 + required FooterType footerType,
196 + required double maxHeight,
197 + MaterialThemeBase? currentTheme,
198 + required this.exchangeTradeViewModel,
199 + required this.onExecuteSwap,
200 + }) : super(
201 + titleText: titleText,
202 + footerType: footerType,
203 + maxHeight: maxHeight,
204 + currentTheme: currentTheme,
205 + );
206 +
207 + final ExchangeTradeViewModel exchangeTradeViewModel;
208 + final VoidCallback onExecuteSwap;
209 +
210 + @override
211 + Widget contentWidget(BuildContext context) {
212 + return Column(
213 + children: [
214 + _SwapDetailsContent(
215 + trade: exchangeTradeViewModel.trade,
216 + exchangeTradeViewModel: exchangeTradeViewModel,
217 + ),
218 + Padding(
219 + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
220 + child: Observer(
221 + builder: (_) {
222 + final sendingState = exchangeTradeViewModel.sendViewModel.state;
223 + final isDisabled = !exchangeTradeViewModel.isSendable ||
224 + exchangeTradeViewModel.trade.inputAddress == null ||
225 + exchangeTradeViewModel.trade.inputAddress!.isEmpty;
226 +
227 + if (isDisabled || sendingState is IsExecutingState) {
228 + return Container(
229 + width: double.infinity,
230 + height: 48,
231 + decoration: BoxDecoration(
232 + borderRadius: BorderRadius.circular(10),
233 + color: Theme.of(context).colorScheme.surfaceContainer,
234 + ),
235 + child: Center(
236 + child: sendingState is IsExecutingState
237 + ? CircularProgressIndicator()
238 + : Text(
239 + 'Swipe to swap',
240 + style: Theme.of(context).textTheme.bodyMedium!.copyWith(
241 + fontSize: 16,
242 + fontWeight: FontWeight.w600,
243 + color: Theme.of(context).colorScheme.onSurfaceVariant,
244 + ),
245 + ),
246 + ),
247 + );
248 + }
249 +
250 + return StandardSlideButton(
251 + tileBackgroundColor: Theme.of(context).colorScheme.surfaceContainer,
252 + knobColor: Theme.of(context).colorScheme.primary,
253 + buttonText: 'Swipe to swap',
254 + onSlideComplete: onExecuteSwap,
255 + currentTheme: currentTheme!,
256 + accessibleNavigationModeButtonText: 'Complete swap',
257 + );
258 + },
259 + ),
260 + ),
261 + const SizedBox(height: 32),
262 + ],
263 + );
264 + }
265 +}
266 +
267 +class _SwapDetailsContent extends StatelessWidget {
268 + const _SwapDetailsContent({required this.trade, required this.exchangeTradeViewModel});
269 +
270 + final Trade trade;
271 + final ExchangeTradeViewModel exchangeTradeViewModel;
272 +
273 + @override
274 + Widget build(BuildContext context) {
275 + return Container(
276 + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 24),
277 + child: Observer(
278 + builder: (_) {
279 + return Column(
280 + mainAxisSize: MainAxisSize.min,
281 + crossAxisAlignment: CrossAxisAlignment.center,
282 + children: [
283 + _SwapDetailsTile(
284 + label: 'You Send',
285 + value: '${trade.amount} ${trade.from?.title ?? ''}',
286 + valueFiatFormatted: exchangeTradeViewModel.sendAmountFiatFormatted,
287 + ),
288 + const SizedBox(height: 8),
289 + _SwapDetailsTile(
290 + label: 'You Get',
291 + value: '${trade.receiveAmount ?? '0'} ${trade.to?.title ?? ''}',
292 + valueFiatFormatted: exchangeTradeViewModel.receiveAmountFiatFormatted,
293 + ),
294 + const SizedBox(height: 8),
295 + Container(
296 + width: double.infinity,
297 + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
298 + decoration: BoxDecoration(
299 + color: Theme.of(context).colorScheme.surfaceContainer,
300 + borderRadius: BorderRadius.circular(10),
301 + ),
302 + child: Column(
303 + crossAxisAlignment: CrossAxisAlignment.start,
304 + children: [
305 + Text(
306 + 'To this Address',
307 + style: Theme.of(context)
308 + .textTheme
309 + .bodyMedium!
310 + .copyWith(fontWeight: FontWeight.w500, fontSize: 16),
311 + ),
312 + const SizedBox(height: 4),
313 + AddressFormatter.buildSegmentedAddress(
314 + address: trade.payoutAddress ?? '',
315 + walletType: trade.to != null ? cryptoCurrencyToWalletType(trade.to!) : null,
316 + evenTextStyle: Theme.of(context)
317 + .textTheme
318 + .bodyMedium!
319 + .copyWith(fontWeight: FontWeight.w600, fontSize: 14),
320 + ),
321 + ],
322 + ),
323 + ),
324 + const SizedBox(height: 8),
325 + Container(
326 + width: double.infinity,
327 + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
328 + decoration: BoxDecoration(
329 + color: Theme.of(context).colorScheme.surfaceContainer,
330 + borderRadius: BorderRadius.circular(12),
331 + ),
332 + child: Row(
333 + children: [
334 + Row(
335 + children: [
336 + CakeImageWidget(
337 + imageUrl: trade.provider.image,
338 + width: 36,
339 + height: 36,
340 + ),
341 + const SizedBox(width: 8),
342 + Text(
343 + trade.provider.title,
344 + style: Theme.of(context).textTheme.bodyMedium!.copyWith(
345 + fontWeight: FontWeight.w500,
346 + fontSize: 16,
347 + ),
348 + ),
349 + ],
350 + ),
351 + Spacer(),
352 + Expanded(
353 + // flex: 4,
354 + child: GestureDetector(
355 + onTap: () {
356 + Clipboard.setData(ClipboardData(text: trade.id));
357 + showBar<void>(context, S.of(context).copied_to_clipboard);
358 + },
359 + child: Row(
360 + children: [
361 + Text(
362 + 'ID: ',
363 + style: Theme.of(context).textTheme.bodyMedium!.copyWith(fontSize: 18),
364 + ),
365 + Expanded(
366 + child: Text(
367 + trade.id,
368 + style:
369 + Theme.of(context).textTheme.bodyMedium!.copyWith(fontSize: 18),
370 + softWrap: true,
371 + ),
372 + ),
373 + ],
374 + ),
375 + ),
376 + ),
377 + ],
378 + ),
379 + ),
380 + ],
381 + );
382 + },
383 + ),
384 + );
385 + }
386 +}
387 +
388 +class _SwapDetailsTile extends StatelessWidget {
389 + const _SwapDetailsTile({
390 + required this.label,
391 + required this.value,
392 + required this.valueFiatFormatted,
393 + });
394 +
395 + final String label;
396 + final String value;
397 + final String valueFiatFormatted;
398 + @override
399 + Widget build(BuildContext context) {
400 + return Container(
401 + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
402 + decoration: BoxDecoration(
403 + color: Theme.of(context).colorScheme.surfaceContainer,
404 + borderRadius: BorderRadius.circular(12),
405 + ),
406 + child: Row(
407 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
408 + children: [
409 + Text(
410 + label,
411 + style: Theme.of(context)
412 + .textTheme
413 + .bodyMedium!
414 + .copyWith(fontWeight: FontWeight.w500, fontSize: 16),
415 + ),
416 + Column(
417 + crossAxisAlignment: CrossAxisAlignment.end,
418 + children: [
419 + Text(
420 + value,
421 + style: Theme.of(context).textTheme.bodyMedium!.copyWith(
422 + fontWeight: FontWeight.w600,
423 + fontSize: 18,
424 + ),
425 + ),
426 + if (valueFiatFormatted.isNotEmpty)
427 + Text(
428 + valueFiatFormatted,
429 + style: Theme.of(context).textTheme.bodyMedium!.copyWith(
430 + fontSize: 10,
431 + fontWeight: FontWeight.w600,
432 + color: Theme.of(context).colorScheme.onSurfaceVariant,
433 + ),
434 + ),
435 + ],
436 + ),
437 + ],
438 + ),
439 + );
440 + }
441 +}
lib/src/widgets/standard_slide_button_widget.dart
+7 -3
@@ -11,6 +11,8 @@ class StandardSlideButton extends StatefulWidget {
11 this.height = 48.0,
12 required this.currentTheme,
13 required this.accessibleNavigationModeButtonText,
14 + this.tileBackgroundColor,
15 + this.knobColor,
16 }) : super(key: key);
17
18 final VoidCallback onSlideComplete;
@@ -18,7 +20,9 @@ class StandardSlideButton extends StatefulWidget {
20 final double height;
21 final MaterialThemeBase currentTheme;
22 final String accessibleNavigationModeButtonText;
21 -
23 + final Color? tileBackgroundColor;
24 + final Color? knobColor;
25 +
26 @override
27 StandardSlideButtonState createState() => StandardSlideButtonState();
28 }
@@ -57,7 +61,7 @@ class StandardSlideButtonState extends State<StandardSlideButton> {
61 height: widget.height,
62 decoration: BoxDecoration(
63 borderRadius: BorderRadius.circular(10),
60 - color: tileBackgroundColor,
64 + color: widget.tileBackgroundColor ?? tileBackgroundColor,
65 ),
66 child: Stack(
67 alignment: Alignment.centerLeft,
@@ -97,7 +101,7 @@ class StandardSlideButtonState extends State<StandardSlideButton> {
101 height: widget.height - 8,
102 decoration: BoxDecoration(
103 borderRadius: BorderRadius.circular(10),
100 - color: Theme.of(context).colorScheme.surface,
104 + color: widget.knobColor ?? Theme.of(context).colorScheme.surface,
105 ),
106 alignment: Alignment.center,
107 child: Icon(
lib/view_model/exchange/exchange_trade_view_model.dart
+68 -33
@@ -1,5 +1,7 @@
1 import 'dart:async';
2
3 +import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
4 +import 'package:cake_wallet/entities/fiat_currency.dart';
5 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
6 import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
7 import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart';
@@ -15,6 +17,7 @@ import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart';
17 import 'package:cake_wallet/exchange/trade.dart';
18 import 'package:cake_wallet/generated/i18n.dart';
19 import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_item.dart';
20 +import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
21 import 'package:cake_wallet/store/dashboard/trades_store.dart';
22 import 'package:cake_wallet/view_model/send/fees_view_model.dart';
23 import 'package:cake_wallet/view_model/send/output.dart';
@@ -36,6 +39,7 @@ abstract class ExchangeTradeViewModelBase with Store {
39 required this.tradesStore,
40 required this.sendViewModel,
41 required this.feesViewModel,
42 + required this.fiatConversionStore,
43 }) : trade = tradesStore.trade!,
44 isSendable = _checkIfCanSend(tradesStore, wallet),
45 items = ObservableList<ExchangeTradeItem>() {
@@ -69,7 +73,7 @@ abstract class ExchangeTradeViewModelBase with Store {
73 case ExchangeProviderDescription.chainflip:
74 _provider = ChainflipExchangeProvider(tradesStore: trades);
75 break;
72 - case ExchangeProviderDescription.xoSwap:
76 + case ExchangeProviderDescription.xoSwap:
77 _provider = XOSwapExchangeProvider();
78 }
79
@@ -116,6 +120,49 @@ abstract class ExchangeTradeViewModelBase with Store {
120
121 Timer? timer;
122
123 + final FiatConversionStore fiatConversionStore;
124 +
125 + FiatCurrency get fiat => sendViewModel.fiat;
126 +
127 + @computed
128 + bool get isFiatDisabled => feesViewModel.isFiatDisabled;
129 +
130 + @computed
131 + String get receiveAmountFiatFormatted {
132 + var amount = '0.00';
133 + try {
134 + if (trade.receiveAmount?.isNotEmpty ?? false) {
135 + if (fiatConversionStore.prices[trade.to] == null) return '';
136 +
137 + amount = calculateFiatAmount(
138 + price: fiatConversionStore.prices[trade.to]!,
139 + cryptoAmount: trade.receiveAmount,
140 + );
141 + }
142 + } catch (_) {
143 + printV('Error calculating receive amount fiat formatted: $_');
144 + }
145 + return isFiatDisabled ? '' : '$amount ${fiat.title}';
146 + }
147 +
148 + @computed
149 + String get sendAmountFiatFormatted {
150 + var amount = '0.00';
151 + try {
152 + if (trade.amount.isNotEmpty) {
153 + if (fiatConversionStore.prices[trade.from] == null) return '';
154 +
155 + amount = calculateFiatAmount(
156 + price: fiatConversionStore.prices[trade.from]!,
157 + cryptoAmount: trade.amount,
158 + );
159 + }
160 + } catch (_) {
161 + printV('Error calculating send amount fiat formatted: $_');
162 + }
163 + return isFiatDisabled ? '' : '$amount ${fiat.title}';
164 + }
165 +
166 void setUpOutput() {
167 sendViewModel.clearOutputs();
168 output = sendViewModel.outputs.first;
@@ -168,13 +215,9 @@ abstract class ExchangeTradeViewModelBase with Store {
215
216 void _updateItems() {
217 final trade = tradesStore.trade!;
171 - final tradeFrom = trade.fromRaw >= 0
172 - ? trade.from
173 - : trade.userCurrencyFrom;
218 + final tradeFrom = trade.fromRaw >= 0 ? trade.from : trade.userCurrencyFrom;
219
175 - final tradeTo = trade.toRaw >= 0
176 - ? trade.to
177 - : trade.userCurrencyTo;
220 + final tradeTo = trade.toRaw >= 0 ? trade.to : trade.userCurrencyTo;
221
222 final tagFrom = tradeFrom?.tag != null ? '${tradeFrom!.tag}' + ' ' : '';
223 final tagTo = tradeTo?.tag != null ? '${tradeTo!.tag}' + ' ' : '';
@@ -221,22 +264,20 @@ abstract class ExchangeTradeViewModelBase with Store {
264 final isExtraIdExist = trade.extraId != null && trade.extraId!.isNotEmpty;
265
266 if (isExtraIdExist) {
224 -
225 -
267 final title = tradeFrom == CryptoCurrency.xrp
227 - ? S.current.destination_tag
228 - : tradeFrom == CryptoCurrency.xlm || tradeFrom == CryptoCurrency.ton
229 - ? S.current.memo
230 - : S.current.extra_id;
231 -
232 - items.add(
233 - ExchangeTradeItem(
234 - title: title,
235 - data: trade.extraId ?? '',
236 - isCopied: true,
237 - isReceiveDetail: !isExtraIdExist,
238 - isExternalSendDetail: isExtraIdExist),
239 - );
268 + ? S.current.destination_tag
269 + : tradeFrom == CryptoCurrency.xlm || tradeFrom == CryptoCurrency.ton
270 + ? S.current.memo
271 + : S.current.extra_id;
272 +
273 + items.add(
274 + ExchangeTradeItem(
275 + title: title,
276 + data: trade.extraId ?? '',
277 + isCopied: true,
278 + isReceiveDetail: !isExtraIdExist,
279 + isExternalSendDetail: isExtraIdExist),
280 + );
281 }
282
283 items.add(
@@ -251,27 +292,21 @@ abstract class ExchangeTradeViewModelBase with Store {
292 }
293
294 static bool _checkIfCanSend(TradesStore tradesStore, WalletBase wallet) {
254 -
295 final trade = tradesStore.trade!;
256 - final tradeFrom = trade.fromRaw >= 0
257 - ? trade.from
258 - : trade.userCurrencyFrom;
296 + final tradeFrom = trade.fromRaw >= 0 ? trade.from : trade.userCurrencyFrom;
297
298 bool _isEthToken() =>
261 - wallet.currency == CryptoCurrency.eth &&
262 - tradeFrom?.tag == CryptoCurrency.eth.title;
299 + wallet.currency == CryptoCurrency.eth && tradeFrom?.tag == CryptoCurrency.eth.title;
300
301 bool _isPolygonToken() =>
302 wallet.currency == CryptoCurrency.maticpoly &&
266 - tradeFrom?.tag == CryptoCurrency.maticpoly.tag;
303 + tradeFrom?.tag == CryptoCurrency.maticpoly.tag;
304
305 bool _isTronToken() =>
269 - wallet.currency == CryptoCurrency.trx &&
270 - tradeFrom?.tag == CryptoCurrency.trx.title;
306 + wallet.currency == CryptoCurrency.trx && tradeFrom?.tag == CryptoCurrency.trx.title;
307
308 bool _isSplToken() =>
273 - wallet.currency == CryptoCurrency.sol &&
274 - tradeFrom?.tag == CryptoCurrency.sol.title;
309 + wallet.currency == CryptoCurrency.sol && tradeFrom?.tag == CryptoCurrency.sol.title;
310
311 return tradeFrom == wallet.currency ||
312 tradesStore.trade!.provider == ExchangeProviderDescription.xmrto ||
lib/view_model/exchange/exchange_view_model.dart
+72 -23
@@ -1,13 +1,18 @@
1 import 'dart:async';
2 import 'dart:collection';
3 import 'dart:convert';
4 +import 'dart:developer';
5
6 import 'package:bitcoin_base/bitcoin_base.dart';
7 import 'package:cake_wallet/.secrets.g.dart' as secrets;
8 import 'package:cake_wallet/bitcoin/bitcoin.dart';
9 import 'package:cake_wallet/core/create_trade_result.dart';
10 +import 'package:cake_wallet/core/fiat_conversion_service.dart';
11 import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
12 +import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
13 import 'package:cake_wallet/entities/exchange_api_mode.dart';
14 +import 'package:cake_wallet/entities/fiat_api_mode.dart';
15 +import 'package:cake_wallet/entities/fiat_currency.dart';
16 import 'package:cake_wallet/entities/preferences_key.dart';
17 import 'package:cake_wallet/entities/wallet_contact.dart';
18 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
@@ -30,6 +35,7 @@ import 'package:cake_wallet/exchange/trade.dart';
35 import 'package:cake_wallet/exchange/trade_request.dart';
36 import 'package:cake_wallet/generated/i18n.dart';
37 import 'package:cake_wallet/store/app_store.dart';
38 +import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
39 import 'package:cake_wallet/store/dashboard/trades_store.dart';
40 import 'package:cake_wallet/store/settings_store.dart';
41 import 'package:cake_wallet/store/templates/exchange_template_store.dart';
@@ -76,6 +82,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
82 this.unspentCoinsListViewModel,
83 this.feesViewModel,
84 this.walletInfoSource,
85 + this.fiatConversionStore,
86 ) : _cryptoNumberFormat = NumberFormat(),
87 isSendAllEnabled = false,
88 isFixedRateMode = false,
@@ -129,8 +136,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
136 autorun((_) {
137 if (selectedProviders.any((provider) => provider is TrocadorExchangeProvider)) {
138 final trocadorProvider =
132 - selectedProviders.firstWhere((provider) => provider is TrocadorExchangeProvider)
133 - as TrocadorExchangeProvider;
139 + selectedProviders.firstWhere((provider) => provider is TrocadorExchangeProvider)
140 + as TrocadorExchangeProvider;
141
142 updateAllTrocadorProviderStates(trocadorProvider);
143 }
@@ -360,6 +367,44 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
367
368 late Timer bestRateSync;
369
370 + final FiatConversionStore fiatConversionStore;
371 +
372 + FiatCurrency get fiat => _settingsStore.fiatCurrency;
373 +
374 + @computed
375 + bool get isFiatDisabled => feesViewModel.isFiatDisabled;
376 +
377 + @action
378 + Future<void> fetchFiatPrice(CryptoCurrency currency) async {
379 + if (fiatConversionStore.prices[currency] != null) {
380 + return;
381 + }
382 +
383 + fiatConversionStore.prices[currency] = await FiatConversionService.fetchPrice(
384 + crypto: currency,
385 + fiat: fiat,
386 + torOnly: _settingsStore.fiatApiMode == FiatApiMode.torOnly,
387 + );
388 + }
389 +
390 + @computed
391 + String get receiveAmountFiatFormatted {
392 + var amount = '0.00';
393 + try {
394 + if (receiveAmount.isNotEmpty) {
395 + if (fiatConversionStore.prices[receiveCurrency] == null) return '';
396 +
397 + amount = calculateFiatAmount(
398 + price: fiatConversionStore.prices[receiveCurrency]!,
399 + cryptoAmount: receiveAmount,
400 + );
401 + }
402 + } catch (_) {
403 + log('Error calculating receive amount fiat formatted: $_');
404 + }
405 + return isFiatDisabled ? '' : '$amount';
406 + }
407 +
408 @action
409 void changeDepositCurrency({required CryptoCurrency currency}) {
410 depositCurrency = currency;
@@ -393,6 +438,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
438
439 await calculateBestRate();
440 }
441 +
442 _cryptoNumberFormat.maximumFractionDigits = depositMaxDigits;
443
444 depositAmount = _cryptoNumberFormat
@@ -401,6 +447,16 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
447 .replaceAll(RegExp('\\,'), '');
448 }
449
450 + @action
451 + void setReceiveAmountFromFiat({required String fiatAmount}) {
452 + final crypto = double.parse(fiatAmount.replaceAll(',', '.')) /
453 + fiatConversionStore.prices[receiveCurrency]!;
454 + final receiveAmountTmp = _cryptoNumberFormat.format(crypto);
455 + if (receiveAmount != receiveAmountTmp) {
456 + changeReceiveAmount(amount: receiveAmountTmp);
457 + }
458 + }
459 +
460 @action
461 Future<void> changeDepositAmount({required String amount}) async {
462 depositAmount = amount;
@@ -444,8 +500,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
500 bestRate = 0.0;
501 return;
502 }
447 - final amount = double.tryParse(isFixedRateMode ? receiveAmount : depositAmount)
448 - ?? initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency);
503 + final amount = double.tryParse(isFixedRateMode ? receiveAmount : depositAmount) ??
504 + initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency);
505
506 final validProvidersForAmount = _tradeAvailableProviders.where((provider) {
507 final limits = _providerLimits[provider];
@@ -515,15 +571,15 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
571 .map((provider) async {
572 final limits = await provider
573 .fetchLimits(
518 - from: from,
519 - to: to,
520 - isFixedRateMode: isFixedRateMode,
521 - )
574 + from: from,
575 + to: to,
576 + isFixedRateMode: isFixedRateMode,
577 + )
578 .onError((error, stackTrace) => Limits(max: 0.0, min: double.maxFinite))
579 .timeout(
524 - Duration(seconds: 7),
525 - onTimeout: () => Limits(max: 0.0, min: double.maxFinite),
526 - );
580 + Duration(seconds: 7),
581 + onTimeout: () => Limits(max: 0.0, min: double.maxFinite),
582 + );
583 return MapEntry(provider, limits);
584 }).toList();
585
@@ -563,8 +619,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
619 Future<void> createTrade() async {
620 if (depositCurrency == receiveCurrency) {
621 tradeState = TradeIsCreatedFailure(
566 - title: S.current.trade_not_created,
567 - error: 'Can\'t exchange the same currency');
622 + title: S.current.trade_not_created, error: 'Can\'t exchange the same currency');
623 return;
624 }
625 if (isSendAllEnabled) {
@@ -881,8 +936,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
936 void _setAvailableProviders() {
937 _tradeAvailableProviders.clear();
938
884 - _tradeAvailableProviders.addAll(
885 - selectedProviders.where((provider) => providerList.contains(provider)));
939 + _tradeAvailableProviders
940 + .addAll(selectedProviders.where((provider) => providerList.contains(provider)));
941 }
942
943 void _setProviders() {
@@ -984,7 +1039,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1039 "X-API-Key": secrets.moralisApiKey,
1040 },
1041 );
987 -
1042
1043 final decodedResponse = jsonDecode(response.body)[0] as Map<String, dynamic>;
1044
@@ -1018,7 +1072,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1072
1073 Future<List<Erc20Token>> _loadAllUniqueEvmTokens() async {
1074 final evmWallets = walletInfoSource.values.where(
1021 - (w) => w.type == WalletType.ethereum || w.type == WalletType.polygon,
1075 + (w) => w.type == WalletType.ethereum || w.type == WalletType.polygon,
1076 );
1077
1078 final seen = <String>{};
@@ -1039,7 +1093,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1093 return unique;
1094 }
1095
1042 -
1096 @action
1097 Future<void> _injectUserEthTokensIntoCurrencyLists() async {
1098 final userTokens = await _loadAllUniqueEvmTokens();
@@ -1134,8 +1187,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1187 }
1188
1189 Future<List<TronToken>> _loadAllUniqueTronTokens() async {
1137 - final tronWallets =
1138 - walletInfoSource.values.where((w) => w.type == WalletType.tron);
1190 + final tronWallets = walletInfoSource.values.where((w) => w.type == WalletType.tron);
1191
1192 final seen = <String>{};
1193 final unique = <TronToken>[];
@@ -1186,11 +1238,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1238 CryptoCurrency.ada => 1000,
1239 CryptoCurrency.dash => 10,
1240 CryptoCurrency.rune => 10,
1189 -
1241 _ => 1
1242 };
1243 return amount.toDouble();
1244 }
1194 -
1195 -
1245 }
lib/view_model/payment/payment_view_model.dart
+28 -11
@@ -44,7 +44,7 @@ abstract class PaymentViewModelBase with Store {
44 return PaymentFlowResult.incompatible('Unable to detect address type');
45 }
46
47 - detectedWalletType = detectionResult.detectedWalletType!;
47 + detectedWalletType = detectionResult.detectedWalletType;
48
49 // Check if current wallet is compatible
50 final currentWallet = appStore.wallet;
@@ -56,11 +56,11 @@ abstract class PaymentViewModelBase with Store {
56
57 switch (compatibleWallets.length) {
58 case 0:
59 - return PaymentFlowResult.noWallets(detectedWalletType!);
59 + return PaymentFlowResult.noWallets(detectedWalletType!, detectionResult);
60 case 1:
61 - return PaymentFlowResult.singleWallet(compatibleWallets.first);
61 + return PaymentFlowResult.singleWallet(compatibleWallets.first, detectionResult);
62 default:
63 - return PaymentFlowResult.multipleWallets(compatibleWallets);
63 + return PaymentFlowResult.multipleWallets(compatibleWallets, detectionResult);
64 }
65 } catch (e) {
66 printV('PaymentViewModel error: $e');
@@ -81,6 +81,7 @@ class PaymentFlowResult {
81 final WalletInfo? wallet;
82 final List<WalletInfo> wallets;
83 final WalletType? walletType;
84 + final AddressDetectionResult? addressDetectionResult;
85
86 PaymentFlowResult._({
87 required this.type,
@@ -88,22 +89,38 @@ class PaymentFlowResult {
89 this.wallet,
90 this.wallets = const [],
91 this.walletType,
92 + this.addressDetectionResult,
93 });
94
95 /// Current wallet is compatible
94 - factory PaymentFlowResult.currentWalletCompatible() => PaymentFlowResult._(type: PaymentFlowType.currentWalletCompatible);
96 + factory PaymentFlowResult.currentWalletCompatible() =>
97 + PaymentFlowResult._(type: PaymentFlowType.currentWalletCompatible);
98
99 /// Single compatible wallet available
97 - factory PaymentFlowResult.singleWallet(WalletInfo wallet) =>
98 - PaymentFlowResult._(type: PaymentFlowType.singleWallet, wallet: wallet);
100 + factory PaymentFlowResult.singleWallet(
101 + WalletInfo wallet, AddressDetectionResult addressDetectionResult) =>
102 + PaymentFlowResult._(
103 + type: PaymentFlowType.singleWallet,
104 + wallet: wallet,
105 + walletType: wallet.type,
106 + addressDetectionResult: addressDetectionResult);
107
108 /// Multiple compatible wallets available
101 - factory PaymentFlowResult.multipleWallets(List<WalletInfo> wallets) =>
102 - PaymentFlowResult._(type: PaymentFlowType.multipleWallets, wallets: wallets);
109 + factory PaymentFlowResult.multipleWallets(
110 + List<WalletInfo> wallets, AddressDetectionResult addressDetectionResult) =>
111 + PaymentFlowResult._(
112 + type: PaymentFlowType.multipleWallets,
113 + wallets: wallets,
114 + walletType: wallets.first.type,
115 + addressDetectionResult: addressDetectionResult);
116
117 /// No compatible wallets available
105 - factory PaymentFlowResult.noWallets(WalletType walletType) =>
106 - PaymentFlowResult._(type: PaymentFlowType.noWallets, walletType: walletType);
118 + factory PaymentFlowResult.noWallets(
119 + WalletType walletType, AddressDetectionResult addressDetectionResult) =>
120 + PaymentFlowResult._(
121 + type: PaymentFlowType.noWallets,
122 + walletType: walletType,
123 + addressDetectionResult: addressDetectionResult);
124
125 /// Error occurred
126 factory PaymentFlowResult.error(String message) =>
lib/view_model/send/output.dart
+11 -1
@@ -11,7 +11,10 @@ import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed
11 import 'package:cake_wallet/tron/tron.dart';
12 import 'package:cake_wallet/wownero/wownero.dart';
13 import 'package:cake_wallet/zano/zano.dart';
14 +import 'package:cw_core/balance.dart';
15 import 'package:cw_core/crypto_currency.dart';
16 +import 'package:cw_core/transaction_history.dart';
17 +import 'package:cw_core/transaction_info.dart';
18 import 'package:cw_core/utils/print_verbose.dart';
19 import 'package:flutter/material.dart';
20 import 'package:intl/intl.dart';
@@ -220,7 +223,8 @@ abstract class OutputBase with Store {
223
224 WalletType get walletType => _wallet.type;
225 final CryptoCurrency Function() cryptoCurrencyHandler;
223 - final WalletBase _wallet;
226 + @observable
227 + WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> _wallet;
228 final SettingsStore _settingsStore;
229 final FiatConversionStore _fiatConversationStore;
230 final NumberFormat _cryptoNumberFormat;
@@ -230,6 +234,12 @@ abstract class OutputBase with Store {
234 sendAll = true;
235 }
236
237 + @action
238 + void updateWallet(WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> newWallet) {
239 + _wallet = newWallet;
240 + _setCryptoNumMaximumFractionDigits();
241 + }
242 +
243 @action
244 void reset() {
245 sendAll = false;
lib/view_model/send/send_view_model.dart
+10
@@ -72,6 +72,16 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
72 wallet.type == WalletType.solana ||
73 wallet.type == WalletType.tron ||
74 wallet.type == WalletType.zano;
75 +
76 + for (final output in outputs) {
77 + output.updateWallet(wallet);
78 + }
79 +
80 + // Update unspent coins list view model with the new wallet reference
81 + unspentCoinsListViewModel.updateWallet(wallet);
82 +
83 + // Update sending balance to reflect the new wallet's balance
84 + updateSendingBalance();
85 }
86
87 UnspentCoinsListViewModel unspentCoinsListViewModel;
lib/view_model/unspent_coins/unspent_coins_list_view_model.dart
+10 -1
@@ -4,6 +4,9 @@ import 'package:cake_wallet/utils/exception_handler.dart';
4 import 'package:cake_wallet/decred/decred.dart';
5 import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart';
6 import 'package:cake_wallet/wownero/wownero.dart';
7 +import 'package:cw_core/balance.dart';
8 +import 'package:cw_core/transaction_history.dart';
9 +import 'package:cw_core/transaction_info.dart';
10 import 'package:cw_core/unspent_coin_type.dart';
11 import 'package:cw_core/unspent_coins_info.dart';
12 import 'package:cw_core/unspent_transaction_output.dart';
@@ -28,7 +31,8 @@ abstract class UnspentCoinsListViewModelBase with Store {
31 items = ObservableList<UnspentCoinsItem>(),
32 _originalState = {};
33
31 - final WalletBase wallet;
34 + @observable
35 + WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet;
36 final Box<UnspentCoinsInfo> _unspentCoinsInfo;
37 final UnspentCoinType coinTypeToSpendFrom;
38
@@ -228,6 +232,11 @@ abstract class UnspentCoinsListViewModelBase with Store {
232 @action
233 void setIsDisposing(bool value) => isDisposing = value;
234
235 + @action
236 + void updateWallet(WalletBase newWallet) {
237 + wallet = newWallet;
238 + }
239 +
240 @action
241 Future<void> dispose() async {
242 await _updateUnspents();