dev
dart 1,038 lines 47.4 KB
Raw
1 import "package:cake_wallet/core/address_resolver/address_resolver_service.dart";
2 import "package:cake_wallet/core/address_validator.dart";
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/exchange_trade_state.dart";
7 import "package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart";
8 import "package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart";
9 import "package:cake_wallet/generated/i18n.dart";
10 import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart";
11 import "package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart";
12 import "package:cake_wallet/new-ui/widgets/modern_button.dart";
13 import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
14 import "package:cake_wallet/new-ui/widgets/send_page/send_syncing_indicator.dart";
15 import "package:cake_wallet/new-ui/widgets/swap_page/anypay_swap_footer.dart";
16 import "package:cake_wallet/new-ui/widgets/swap_page/provider_selector_page.dart";
17 import "package:cake_wallet/new-ui/widgets/swap_page/swap_amount_box.dart";
18 import "package:cake_wallet/new-ui/widgets/swap_page/swap_confirm_sheet.dart";
19 import "package:cake_wallet/new-ui/widgets/swap_page/swap_from_send_args.dart";
20 import "package:cake_wallet/new-ui/widgets/swap_page/swap_limit_popup.dart";
21 import "package:cake_wallet/new-ui/widgets/swap_page/swap_options_page.dart";
22 import "package:cake_wallet/new-ui/widgets/swap_page/swap_section_header.dart";
23 import "package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart";
24 import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
25 import "package:cake_wallet/src/widgets/alert_with_two_actions.dart";
26 import "package:cake_wallet/src/widgets/cake_image_widget.dart";
27 import "package:cake_wallet/src/widgets/primary_button.dart";
28 import "package:cake_wallet/utils/debounce.dart";
29 import "package:cake_wallet/utils/payment_request.dart";
30 import "package:cake_wallet/utils/show_pop_up.dart";
31 import "package:cake_wallet/view_model/dashboard/balance_view_model.dart";
32 import "package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart";
33 import "package:cake_wallet/view_model/exchange/exchange_view_model.dart";
34 import "package:cake_wallet/view_model/wallet_switcher_view_model.dart";
35 import "package:cw_core/crypto_currency.dart";
36 import "package:cw_core/currency_for_wallet_type.dart";
37 import "package:cw_core/sync_status.dart";
38 import "package:cw_core/utils/print_verbose.dart";
39 import "package:cw_core/wallet_type.dart";
40 import "package:flutter/cupertino.dart";
41 import "package:flutter/material.dart";
42 import "package:flutter/services.dart";
43 import "package:flutter_mobx/flutter_mobx.dart";
44 import "package:mobx/mobx.dart";
45 import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
46
47 class NewSwapPage extends StatefulWidget {
48 NewSwapPage(
49 this.exchangeViewModel,
50 this.authService,
51 this.adrResService,
52 this.initialPaymentRequest, {
53 required this.walletSwitcherViewModel,
54 CryptoCurrency? initialCurrency,
55 this.fromSend,
56 this.balanceViewModel,
57 }) {
58 depositWalletName = exchangeViewModel.depositCurrency == CryptoCurrency.xmr
59 ? exchangeViewModel.wallet.name
60 : null;
61 receiveWalletName = exchangeViewModel.receiveCurrency == CryptoCurrency.xmr
62 ? exchangeViewModel.wallet.name
63 : null;
64 if (initialCurrency != null) {
65 exchangeViewModel.changeDepositCurrency(currency: initialCurrency);
66 }
67 }
68
69 final ExchangeViewModel exchangeViewModel;
70 final WalletSwitcherViewModel walletSwitcherViewModel;
71 final AuthService authService;
72 final AddressResolverService adrResService;
73 final PaymentRequest? initialPaymentRequest;
74 final SwapFromSendArgs? fromSend;
75 final BalanceViewModel? balanceViewModel;
76 late final String? depositWalletName;
77 late final String? receiveWalletName;
78
79 @override
80 State<NewSwapPage> createState() => _NewSwapPageState();
81 }
82
83 class _NewSwapPageState extends State<NewSwapPage> {
84 final depositKey = GlobalKey<SwapAmountBoxState>();
85 final receiveKey = GlobalKey<SwapAmountBoxState>();
86 final formKey = GlobalKey<FormState>();
87 final _depositAmountFocus = FocusNode();
88 final _depositAddressFocus = FocusNode();
89 final _receiveAmountFocus = FocusNode();
90 final _receiveAddressFocus = FocusNode();
91 final _receiveAmountDebounce = Debounce(const Duration(milliseconds: 500));
92 Debounce _depositAmountDebounce = Debounce(const Duration(milliseconds: 500));
93 final List<ReactionDisposer> _disposers = [];
94
95 bool get _shouldWaitTillSynced =>
96 [CryptoCurrency.xmr, CryptoCurrency.btc, CryptoCurrency.ltc]
97 .contains(widget.exchangeViewModel.depositCurrency) &&
98 !(widget.exchangeViewModel.status is SyncedSyncStatus);
99
100 Map<CryptoCurrency, CurrencyPickerBalance>? _depositBalanceByAsset() {
101 final balanceViewModel = widget.balanceViewModel;
102 if (balanceViewModel == null) {
103 return null;
104 }
105
106 return {
107 for (final r in balanceViewModel.formattedBalances)
108 r.asset: CurrencyPickerBalance(
109 amount: "${r.availableBalance} ${r.asset.title}",
110 fiat: balanceViewModel.isFiatDisabled
111 ? null
112 : "${r.fiatAvailableBalanceRaw} ${r.fiatCurrency?.symbol}",
113 fiatValue:
114 balanceViewModel.isFiatDisabled ? null : double.tryParse(r.fiatAvailableBalanceRaw),
115 ),
116 };
117 }
118
119 @override
120 void initState() {
121 super.initState();
122 if (widget.exchangeViewModel.feesViewModel.isLowFee) {
123 _showFeeAlert(context);
124 }
125
126 WidgetsBinding.instance.addPostFrameCallback((_) {
127 if (!mounted) {
128 return;
129 }
130
131 // if (!widget.exchangeViewModel.decentralizedExchangesPromptDismissed) {
132 // showMaterialModalBottomSheet(
133 // context: context,
134 // backgroundColor: Colors.transparent,
135 // isDismissible: false,
136 // builder: (context) {
137 // return SwapProviderInitialPreferenceModal();
138 // }).then((val) {
139 // widget.exchangeViewModel.dismissDecentralizedExchangesPrompt();
140 // if (val is bool && val == true && !widget.exchangeViewModel.forceDecentralizedExchanges) {
141 // widget.exchangeViewModel.toggleForceDecentralizedExchanges();
142 // }
143 // });
144 // }
145
146 final depositAddressController = depositKey.currentState!.addressController;
147 final depositAmountController = depositKey.currentState!.amountController;
148 final receiveAddressController = receiveKey.currentState!.addressController;
149 final receiveAmountController = receiveKey.currentState!.amountController;
150 final depositFiatAmountController = depositKey.currentState!.fiatAmountController;
151 final receiveFiatAmountController = receiveKey.currentState!.fiatAmountController;
152
153 depositFiatAmountController.addListener(() {
154 if (!depositKey.currentState!.amountFocusNode.hasFocus) {
155 return;
156 }
157 widget.exchangeViewModel.isFixedRateMode = false;
158 widget.exchangeViewModel.isSendAllEnabled = false;
159 Future.delayed(const Duration(milliseconds: 200)).then((_) {
160 if (!mounted) {
161 return;
162 }
163 if (double.tryParse(depositFiatAmountController.text) != null) {
164 widget.exchangeViewModel
165 .setDepositAmountFromFiat(fiatAmount: depositFiatAmountController.text);
166 receiveKey.currentState?.updateFiatAmount();
167 }
168 });
169 });
170 receiveFiatAmountController.addListener(() {
171 if (!receiveKey.currentState!.amountFocusNode.hasFocus) {
172 return;
173 }
174
175 widget.exchangeViewModel.enableFixedRateMode();
176 widget.exchangeViewModel.isSendAllEnabled = false;
177 Future.delayed(const Duration(milliseconds: 200)).then((_) {
178 if (!mounted) {
179 return;
180 }
181 if (double.tryParse(receiveFiatAmountController.text) != null) {
182 String text = receiveFiatAmountController.text;
183 if (text.contains(".")) {
184 text = text.replaceAll(RegExp(r"0+$"), "");
185 text = text.replaceAll(RegExp(r"\.$"), "");
186 }
187 widget.exchangeViewModel
188 .setReceiveAmountFromFiat(fiatAmount: receiveFiatAmountController.text);
189 depositKey.currentState!.updateFiatAmount();
190 }
191 });
192 });
193 _disposers.add(reaction((_) => widget.exchangeViewModel.isFixedRateMode, (val) {
194 Future.delayed(const Duration(seconds: 3)).then((_) {
195 if (val) {
196 if (depositKey.currentState?.mounted == true) {
197 depositKey.currentState!.updateFiatAmount();
198 }
199 } else {
200 if (receiveKey.currentState?.mounted == true) {
201 receiveKey.currentState!.updateFiatAmount();
202 }
203 }
204 });
205 }));
206
207 _disposers.add(reaction((_) => widget.exchangeViewModel.depositAmount, (String amount) {
208 if (depositKey.currentState!.amountFocusNode.hasFocus) {
209 return;
210 }
211
212 if (widget.exchangeViewModel.isSendAllEnabled) {
213 depositAmountController.text = S.of(context).all;
214 } else if (depositAmountController.text != amount && amount != S.of(context).all) {
215 depositAmountController.text = amount;
216 }
217 }));
218
219 _onCurrencyChange(
220 widget.exchangeViewModel.receiveCurrency,
221 widget.exchangeViewModel,
222 receiveKey,
223 );
224 _onCurrencyChange(
225 widget.exchangeViewModel.depositCurrency,
226 widget.exchangeViewModel,
227 depositKey,
228 );
229
230 _disposers.add(reaction(
231 (_) => widget.exchangeViewModel.wallet.name,
232 (_) => _onWalletNameChange(
233 widget.exchangeViewModel,
234 widget.exchangeViewModel.receiveCurrency,
235 receiveKey,
236 ),
237 ));
238
239 _disposers.add(reaction(
240 (_) => widget.exchangeViewModel.wallet.name,
241 (_) => _onWalletNameChange(
242 widget.exchangeViewModel,
243 widget.exchangeViewModel.depositCurrency,
244 depositKey,
245 ),
246 ));
247
248 _disposers.add(reaction(
249 (_) => widget.exchangeViewModel.receiveCurrency,
250 (currency) => _onCurrencyChange(currency, widget.exchangeViewModel, receiveKey),
251 ));
252
253 _disposers.add(reaction(
254 (_) => widget.exchangeViewModel.depositCurrency,
255 (currency) => _onCurrencyChange(currency, widget.exchangeViewModel, depositKey),
256 ));
257
258 _disposers.add(reaction((_) => widget.exchangeViewModel.depositAddress, (String address) {
259 if (depositKey.currentState!.addressController.text != address) {
260 depositKey.currentState!.addressController.text = address;
261 }
262 }));
263
264 _disposers
265 .add(reaction((_) => widget.exchangeViewModel.isDepositAddressEnabled, (isEnabled) {}));
266
267 _disposers.add(reaction((_) => widget.exchangeViewModel.receiveAmount, (String amount) {
268 if (receiveKey.currentState!.amountController.text != amount) {
269 receiveKey.currentState!.amountController.text = amount;
270 }
271 }));
272
273 _disposers.add(reaction((_) => widget.exchangeViewModel.receiveAddress, (String address) {
274 if (receiveKey.currentState!.addressController.text != address) {
275 receiveKey.currentState!.addressController.text = address;
276 }
277 }));
278
279 _disposers.add(reaction(
280 (_) => widget.exchangeViewModel.isReceiveAmountEditable,
281 (bool isReceiveAmountEditable) {},
282 ));
283
284 _disposers.add(
285 reaction((_) => widget.exchangeViewModel.tradeState, (ExchangeTradeState state) async {
286 if (state is TradeIsCreatedFailure) {
287 WidgetsBinding.instance.addPostFrameCallback((_) {
288 if (mounted) {
289 showPopUp<void>(
290 context: context,
291 builder: (BuildContext context) => AlertWithOneAction(
292 key: const ValueKey("exchange_page_trade_creation_failure_dialog_key"),
293 buttonKey:
294 const ValueKey("exchange_page_trade_creation_failure_dialog_button_key"),
295 alertTitle: S.of(context).provider_error(state.title),
296 alertContent: state.error,
297 buttonText: S.of(context).ok,
298 buttonAction: () => Navigator.of(context).pop(),
299 ),
300 );
301 }
302 });
303 }
304 if (state is TradeIsCreatedSuccessfully) {
305 if (widget.exchangeViewModel.tradeStarted) {
306 return;
307 }
308 final receiveAmount = widget.exchangeViewModel.receiveAmount;
309 // FIXME we don't know why a reset is/was needed here, it messes up ui so i removed it
310 // widget.exchangeViewModel.reset();
311 // (widget.exchangeViewModel.tradesStore.trade?.provider ==
312 // ExchangeProviderDescription.thorChain ||
313 // widget.exchangeViewModel.tradesStore.trade?.provider ==
314 // ExchangeProviderDescription.chainflip)
315 // ? Navigator.of(context).pushReplacementNamed(Routes.exchangeTrade)
316 // : Navigator.of(context).pushReplacementNamed(Routes.exchangeConfirm);
317 widget.exchangeViewModel.tradeStarted = true;
318 final vm = getIt.get<ExchangeTradeViewModel>();
319 final page = SwapConfirmSheet(
320 exchangeViewModel: widget.exchangeViewModel,
321 exchangeTradeViewModel: vm,
322 receiveAmount: receiveAmount,
323 );
324 try {
325 await showMaterialModalBottomSheet<void>(
326 context: context,
327 builder: (context) => page,
328 backgroundColor: Colors.transparent,
329 );
330 } finally {
331 widget.exchangeViewModel.tradeStarted = false;
332 }
333 }
334 }));
335
336 _disposers.add(reaction((_) => widget.exchangeViewModel.bestRate, (double rate) {
337 if (widget.exchangeViewModel.isFixedRateMode) {
338 widget.exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text);
339 } else {
340 printV("bestrate");
341 if ((depositKey.currentState?.fiatInputMode ?? false) ||
342 depositAmountController.text.isEmpty ||
343 depositAmountController.text == S.current.all) {
344 widget.exchangeViewModel
345 .changeDepositAmount(amount: widget.exchangeViewModel.depositAmount);
346 } else {
347 widget.exchangeViewModel.changeDepositAmount(amount: depositAmountController.text);
348 }
349 }
350 }));
351
352 _disposers.add(reaction((_) => widget.exchangeViewModel.forcedProviderRate, (double rate) {
353 if (widget.exchangeViewModel.forcedProvider != null && rate == 0) {
354 return;
355 }
356 if (widget.exchangeViewModel.isFixedRateMode) {
357 widget.exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text);
358 } else if (depositAmountController.text.isNotEmpty &&
359 depositAmountController.text != S.current.all) {
360 widget.exchangeViewModel.changeDepositAmount(amount: depositAmountController.text);
361 }
362 }));
363
364 depositAddressController.addListener(
365 () => widget.exchangeViewModel.depositAddress = depositAddressController.text,
366 );
367
368 depositAmountController.addListener(() {
369 if (depositAmountController.text != widget.exchangeViewModel.depositAmount &&
370 depositAmountController.text != S.of(context).all) {
371 widget.exchangeViewModel.isSendAllEnabled = false;
372 final isThorChain = widget.exchangeViewModel.selectedProviders
373 .any((provider) => provider is ThorChainExchangeProvider);
374 final isChainflip = widget.exchangeViewModel.selectedProviders
375 .any((provider) => provider is ChainflipExchangeProvider);
376
377 _depositAmountDebounce = isThorChain || isChainflip
378 ? Debounce(const Duration(milliseconds: 1000))
379 : Debounce(const Duration(milliseconds: 500));
380
381 _depositAmountDebounce.run(() {
382 widget.exchangeViewModel.calculateBestRate();
383 if (depositAmountController.text != widget.exchangeViewModel.depositAmount &&
384 depositAmountController.text != S.of(context).all) {
385 widget.exchangeViewModel.changeDepositAmount(amount: depositAmountController.text);
386 }
387 widget.exchangeViewModel.isReceiveAmountEntered = false;
388 widget.exchangeViewModel.isFixedRateMode = false;
389 if (receiveKey.currentState != null &&
390 !receiveKey.currentState!.amountFocusNode.hasFocus) {
391 receiveKey.currentState!.updateFiatAmount();
392 }
393 });
394 }
395 });
396
397 receiveAddressController.addListener(
398 () => widget.exchangeViewModel.receiveAddress = receiveAddressController.text,
399 );
400
401 receiveAmountController.addListener(() {
402 if (receiveAmountController.text != widget.exchangeViewModel.receiveAmount) {
403 _receiveAmountDebounce.run(() {
404 widget.exchangeViewModel.calculateBestRate();
405 widget.exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text);
406 widget.exchangeViewModel.isReceiveAmountEntered = true;
407 widget.exchangeViewModel.enableFixedRateMode();
408 if (!depositKey.currentState!.amountFocusNode.hasFocus) {
409 depositKey.currentState!.updateFiatAmount();
410 }
411 });
412 }
413 });
414
415 _disposers.add(
416 reaction((_) => widget.exchangeViewModel.wallet.walletAddresses.addressForExchange,
417 (String address) {
418 if (widget.exchangeViewModel.depositCurrency == CryptoCurrency.xmr) {
419 depositKey.currentState!.changeAddress(address: address);
420 }
421
422 // if (widget.exchangeViewModel.receiveCurrency == CryptoCurrency.xmr) {
423 // receiveKey.currentState!.changeAddress(address: address);
424 // }
425 }));
426
427 _depositAddressFocus.addListener(() async {
428 if (!_depositAddressFocus.hasFocus && depositAddressController.text.isNotEmpty) {
429 final domain = depositAddressController.text;
430 widget.exchangeViewModel.depositAddress =
431 await fetchParsedAddress(context, domain, widget.exchangeViewModel.depositCurrency);
432 }
433 });
434
435 _receiveAddressFocus.addListener(() async {
436 if (!_receiveAddressFocus.hasFocus && receiveAddressController.text.isNotEmpty) {
437 final domain = receiveAddressController.text;
438 widget.exchangeViewModel.receiveAddress =
439 await fetchParsedAddress(context, domain, widget.exchangeViewModel.receiveCurrency);
440 }
441 });
442
443 _receiveAmountFocus.addListener(() {
444 if (_receiveAmountFocus.hasFocus) {
445 widget.exchangeViewModel.enableFixedRateMode();
446 }
447 // exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text);
448 });
449
450 _depositAmountFocus.addListener(() {
451 widget.exchangeViewModel.isFixedRateMode = false;
452 // exchangeViewModel.changeDepositAmount(
453 // amount: depositAmountController.text);
454 });
455
456 if (widget.initialPaymentRequest != null) {
457 try {
458 widget.exchangeViewModel.receiveCurrency =
459 CryptoCurrency.fromString(widget.initialPaymentRequest!.scheme);
460 widget.exchangeViewModel.setCanonicalReceiveAmount(widget.initialPaymentRequest!.amount);
461 widget.exchangeViewModel.receiveAddress = widget.initialPaymentRequest!.address;
462 } catch (e) {
463 printV("error: ${e.toString()}");
464 // TODO
465 }
466 }
467
468 if (widget.fromSend != null) {
469 widget.exchangeViewModel.changeReceiveCurrency(currency: widget.fromSend!.receiveCurrency);
470 widget.exchangeViewModel.receiveAddress = widget.fromSend!.recipientAddress;
471 widget.exchangeViewModel.depositAddress =
472 widget.exchangeViewModel.wallet.walletAddresses.addressForExchange;
473 widget.exchangeViewModel.enableFixedRateMode();
474 final receiveAmount = widget.fromSend!.receiveAmount;
475 if (receiveAmount != null) {
476 widget.exchangeViewModel.setCanonicalReceiveAmount(receiveAmount.toString());
477 }
478 widget.exchangeViewModel.calculateBestRate();
479 }
480 });
481 }
482
483 @override
484 void dispose() {
485 for (final disposer in _disposers) {
486 disposer();
487 }
488 _receiveAmountDebounce.cancel();
489 _depositAmountDebounce.cancel();
490 _depositAmountFocus.dispose();
491 _depositAddressFocus.dispose();
492 _receiveAmountFocus.dispose();
493 _receiveAddressFocus.dispose();
494 widget.exchangeViewModel.dispose();
495 super.dispose();
496 }
497
498 void _onCurrencyChange(
499 CryptoCurrency currency,
500 ExchangeViewModel exchangeViewModel,
501 GlobalKey<SwapAmountBoxState> key,
502 ) {
503 final isCurrentTypeWallet = exchangeViewModel.useSameWalletAddress(currency);
504
505 if (key == depositKey && !isCurrentTypeWallet) {
506 exchangeViewModel.isSendFromExternal = true;
507 }
508 if (key == depositKey && isCurrentTypeWallet) {
509 exchangeViewModel.isSendFromExternal = false;
510 }
511
512 if (key == depositKey) {
513 key.currentState!.changeAddress(
514 address:
515 isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.addressForExchange : "",
516 );
517 }
518 }
519
520 void _onWalletNameChange(
521 ExchangeViewModel exchangeViewModel,
522 CryptoCurrency currency,
523 GlobalKey<SwapAmountBoxState> key,
524 ) {
525 final isCurrentTypeWallet = exchangeViewModel.useSameWalletAddress(currency);
526
527 if (isCurrentTypeWallet) {
528 key.currentState!.addressController.text =
529 exchangeViewModel.wallet.walletAddresses.addressForExchange;
530 } else if (key.currentState!.addressController.text ==
531 exchangeViewModel.wallet.walletAddresses.addressForExchange) {
532 key.currentState!.addressController.text = "";
533 }
534 }
535
536 Future<String> fetchParsedAddress(
537 BuildContext context,
538 String domain,
539 CryptoCurrency currency,
540 ) async {
541 printV("$domain");
542 final parsedAddress = await widget.adrResService
543 .resolve(query: domain, wallet: widget.exchangeViewModel.wallet, currency: currency);
544 return parsedAddress.isNotEmpty
545 ? (parsedAddress.first.parsedAddressByCurrencyMap[currency] ?? domain)
546 : domain;
547 }
548
549 void _showFeeAlert(BuildContext context) async {
550 await Future<void>.delayed(const Duration(seconds: 1));
551 if (!context.mounted) {
552 return;
553 }
554
555 final confirmed = await showPopUp<bool>(
556 context: context,
557 builder: (dialogContext) => AlertWithTwoActions(
558 alertTitle: S.of(context).low_fee,
559 alertContent: S.of(context).low_fee_alert,
560 leftButtonText: S.of(context).ignor,
561 rightButtonText: S.of(context).use_suggested,
562 actionLeftButton: () => Navigator.of(dialogContext).pop(false),
563 actionRightButton: () => Navigator.of(dialogContext).pop(true),
564 ),
565 ) ??
566 false;
567 if (confirmed) {
568 widget.exchangeViewModel.feesViewModel.setDefaultTransactionPriority();
569 }
570 }
571
572 @override
573 Widget build(BuildContext context) {
574 final colorScheme = Theme.of(context).colorScheme;
575
576 final fromSend = widget.fromSend;
577 final fromType = widget.exchangeViewModel.wallet.type;
578 final fromName = walletTypeToString(fromType);
579 final fromIcon = symbolIconPathForWalletType(fromType) ?? "";
580
581 final targetType = fromSend?.targetWalletType;
582 final toName = targetType != null ? walletTypeToString(targetType) : "";
583 final toIcon = (targetType != null ? symbolIconPathForWalletType(targetType) : null) ?? "";
584
585 return KeyboardHideOverlay(
586 unfocusOnTap: true,
587 child: Container(
588 decoration: BoxDecoration(
589 color: colorScheme.surface,
590 borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
591 ),
592 child: Column(
593 children: [
594 ModalTopBar(
595 title:
596 fromSend != null ? S.of(context).swap_from_network(fromName) : S.of(context).swap,
597 leadingIcon: Icon(fromSend != null ? Icons.arrow_back_ios_new : Icons.close),
598 leadingSemanticLabel: S.of(context).close,
599 onLeadingPressed: Navigator.of(context).maybePop,
600 trailingIcon: fromSend != null
601 ? null
602 : CakeImageWidget(
603 imageUrl: "assets/new-ui/options.svg",
604 colorFilter: ColorFilter.mode(colorScheme.primary, BlendMode.srcIn),
605 ),
606 trailingSemanticLabel: S.of(context).configure,
607 onTrailingPressed: () {
608 Navigator.of(context).push(
609 CupertinoPageRoute(
610 builder: (context) => Material(
611 child: SwapOptionsPage(exchangeViewModel: widget.exchangeViewModel),
612 ),
613 ),
614 );
615 },
616 ),
617 const SizedBox(height: 24),
618 Expanded(
619 child: SafeArea(
620 child: Padding(
621 padding: const EdgeInsets.symmetric(horizontal: 18),
622 child: Column(
623 mainAxisAlignment: MainAxisAlignment.spaceBetween,
624 mainAxisSize: MainAxisSize.max,
625 children: [
626 Expanded(
627 child: RepaintBoundary(
628 child: Form(
629 key: formKey,
630 child: SingleChildScrollView(
631 physics: const ClampingScrollPhysics(),
632 controller: ModalScrollController.of(context),
633 child: Column(
634 children: [
635 if (fromSend != null)
636 Padding(
637 padding: const EdgeInsets.only(bottom: 12),
638 child: SwapSectionHeader(
639 label: S.of(context).from,
640 networkName: fromName,
641 networkIconPath: fromIcon,
642 ),
643 ),
644 Observer(
645 builder: (_) => SwapAmountBox(
646 isReceiverCard: false,
647 walletSwitcherViewModel: widget.walletSwitcherViewModel,
648 exchangeViewModel: widget.exchangeViewModel,
649 hasAllAmount: widget.exchangeViewModel.hasAllAmount,
650 allAmount: widget.exchangeViewModel.hasAllAmount
651 ? () => widget.exchangeViewModel.enableSendAllAmount()
652 : null,
653 key: depositKey,
654 title: fromSend != null ? "" : S.of(context).send,
655 sourceSelectorMode: fromSend != null,
656 walletName: fromSend != null
657 ? widget.exchangeViewModel.wallet.name
658 : null,
659 balanceByAsset:
660 fromSend != null ? _depositBalanceByAsset() : null,
661 useSingleNetworkLayout: fromSend != null,
662 filteredNetwork: fromSend != null
663 ? widget.exchangeViewModel.wallet.type
664 : null,
665 currency: widget.exchangeViewModel.depositCurrency,
666 useBaseUnit: widget.exchangeViewModel.useDepositBaseUnit,
667 hasRefundAddress: true,
668 currencies: widget.exchangeViewModel.depositCurrencies,
669 onCurrencySelected: (currency) {
670 if (currency is! CryptoCurrency) {
671 return;
672 }
673 final vm = widget.exchangeViewModel;
674 if (fromSend == null) {
675 vm.changeDepositCurrency(currency: currency);
676 return;
677 }
678 final keepReceive = vm.receiveAmount;
679 vm.changeDepositCurrency(currency: currency);
680 vm.enableFixedRateMode();
681 if (keepReceive.isNotEmpty) {
682 vm.changeReceiveAmount(amount: keepReceive);
683 } else {
684 vm.calculateBestRate();
685 }
686 },
687 currencyValueValidator: (value) => !widget
688 .exchangeViewModel.isFixedRateMode &&
689 value != S.of(context).all
690 ? AmountValidator(
691 isAutovalidate: true,
692 currency: widget.exchangeViewModel.isFixedRateMode
693 ? widget.exchangeViewModel.receiveCurrency
694 : widget.exchangeViewModel.depositCurrency,
695 minValue:
696 widget.exchangeViewModel.limits.min.toString(),
697 maxValue:
698 widget.exchangeViewModel.limits.max.toString(),
699 amountParsingProxy:
700 widget.exchangeViewModel.amountParsingProxy,
701 ).call(value)
702 : null,
703 addressTextFieldValidator: AddressValidator(
704 type: widget.exchangeViewModel.depositCurrency,
705 ),
706 onPushPasteButton: (context) async {
707 final clipboard = await Clipboard.getData("text/plain");
708 widget.exchangeViewModel.depositAddress =
709 clipboard?.text ?? "";
710
711 final domain = widget.exchangeViewModel.depositAddress;
712 widget.exchangeViewModel.depositAddress =
713 await fetchParsedAddress(
714 context,
715 domain,
716 widget.exchangeViewModel.depositCurrency,
717 );
718 },
719 onPushAddressBookButton: (context) async {
720 final domain = widget.exchangeViewModel.depositAddress;
721 widget.exchangeViewModel.depositAddress =
722 await fetchParsedAddress(
723 context,
724 domain,
725 widget.exchangeViewModel.depositCurrency,
726 );
727 },
728 ),
729 ),
730 SwapLimitPopup(exchangeViewModel: widget.exchangeViewModel),
731 if (fromSend != null)
732 Column(
733 children: [
734 const SizedBox(height: 24),
735 Icon(
736 Icons.arrow_downward,
737 size: 24,
738 color: colorScheme.onSurfaceVariant,
739 semanticLabel: S.of(context).swap_reverse_direction,
740 ),
741 const SizedBox(height: 24),
742 ],
743 )
744 else
745 Padding(
746 padding: const EdgeInsets.symmetric(vertical: 12),
747 child: Stack(
748 alignment: Alignment.center,
749 children: [
750 Container(
751 height: 1,
752 width: double.infinity,
753 color: colorScheme.surfaceContainerHigh,
754 ),
755 ModernButton.svg(
756 size: 36,
757 iconSize: 24,
758 svgPath: "assets/new-ui/swap_amounts.svg",
759 onPressed:
760 widget.exchangeViewModel.reverseSwapDirection,
761 ),
762 ],
763 ),
764 ),
765 if (fromSend != null)
766 Padding(
767 padding: const EdgeInsets.only(bottom: 12),
768 child: SwapSectionHeader(
769 label: S.of(context).to,
770 networkName: toName,
771 networkIconPath: toIcon,
772 ),
773 ),
774 Observer(
775 builder: (_) => SwapAmountBox(
776 isReceiverCard: true,
777 walletSwitcherViewModel: widget.walletSwitcherViewModel,
778 exchangeViewModel: widget.exchangeViewModel,
779 key: receiveKey,
780 title: fromSend != null ? "" : S.of(context).receive,
781 filteredNetwork:
782 fromSend != null ? fromSend.targetWalletType : null,
783 currencies: widget.exchangeViewModel.receiveCurrencies,
784 currency: widget.exchangeViewModel.receiveCurrency,
785 useBaseUnit: widget.exchangeViewModel.useReceiveBaseUnit,
786 onCurrencySelected: (currency) {
787 if (currency is! CryptoCurrency) {
788 return;
789 }
790 final vm = widget.exchangeViewModel;
791 final keepAddress = vm.receiveAddress;
792 vm.changeReceiveCurrency(currency: currency);
793 if (fromSend != null) {
794 if (keepAddress.isNotEmpty) {
795 vm.receiveAddress = keepAddress;
796 }
797 vm.enableFixedRateMode();
798 vm.calculateBestRate();
799 }
800 },
801 currencyValueValidator: (value) => widget
802 .exchangeViewModel.isFixedRateMode
803 ? AmountValidator(
804 isAutovalidate: true,
805 currency: widget.exchangeViewModel.receiveCurrency,
806 minValue:
807 widget.exchangeViewModel.limits.min.toString(),
808 maxValue:
809 widget.exchangeViewModel.limits.max.toString(),
810 amountParsingProxy:
811 widget.exchangeViewModel.amountParsingProxy,
812 ).call(value)
813 : null,
814 addressTextFieldValidator: AddressValidator(
815 type: widget.exchangeViewModel.receiveCurrency,
816 ),
817 onPushPasteButton: (context) async {
818 final clipboard = await Clipboard.getData("text/plain");
819 widget.exchangeViewModel.receiveAddress =
820 clipboard?.text ?? "";
821
822 final domain = widget.exchangeViewModel.receiveAddress;
823 widget.exchangeViewModel.receiveAddress =
824 await fetchParsedAddress(
825 context,
826 domain,
827 widget.exchangeViewModel.receiveCurrency,
828 );
829 },
830 onPushAddressBookButton: (context) async {
831 final domain = widget.exchangeViewModel.receiveAddress;
832 widget.exchangeViewModel.receiveAddress =
833 await fetchParsedAddress(
834 context,
835 domain,
836 widget.exchangeViewModel.receiveCurrency,
837 );
838 },
839 ),
840 ),
841 ],
842 ),
843 ),
844 ),
845 ),
846 ),
847 Observer(
848 builder: (_) => Column(
849 spacing: 12,
850 mainAxisAlignment: MainAxisAlignment.end,
851 children: [
852 if (widget.exchangeViewModel.status is! SyncedSyncStatus)
853 SendSyncingIndicator(status: widget.exchangeViewModel.status),
854 if (fromSend == null && widget.exchangeViewModel.isFixedRateMode)
855 Text(
856 S.of(context).exchange_rate_is_fixed,
857 style: TextStyle(color: colorScheme.onSurfaceVariant, fontSize: 12),
858 ),
859 if (fromSend != null)
860 AnyPaySwapFooter(exchangeViewModel: widget.exchangeViewModel)
861 else
862 SwapProviderPreview(exchangeViewModel: widget.exchangeViewModel),
863 Observer(
864 builder: (_) => LoadingPrimaryButton(
865 key: const ValueKey("exchange_page_exchange_button_key"),
866 text: widget.exchangeViewModel.isAvailableInSelected
867 ? S.of(context).swap
868 : S.of(context).change_selected_exchanges,
869 onPressed: widget.exchangeViewModel.isAvailableInSelected
870 ? () {
871 FocusScope.of(context).unfocus();
872 if (formKey.currentState != null &&
873 formKey.currentState!.validate()) {
874 if (_shouldWaitTillSynced) {
875 showPopUp<void>(
876 context: context,
877 builder: (BuildContext context) => AlertWithOneAction(
878 alertTitle: S.of(context).exchange,
879 alertContent:
880 S.of(context).exchange_sync_alert_content,
881 buttonText: S.of(context).ok,
882 buttonAction: () => Navigator.of(context).pop(),
883 ),
884 );
885 } else {
886 final check =
887 widget.exchangeViewModel.shouldDisplayTOTP();
888 widget.authService.authenticateAction(
889 context,
890 conditionToDetermineIfToUse2FA: check,
891 onAuthSuccess: (value) {
892 if (value) {
893 widget.exchangeViewModel.createTrade();
894 }
895 },
896 );
897 }
898 }
899 }
900 : () => PresentProviderPicker(
901 exchangeViewModel: widget.exchangeViewModel,
902 ).presentProviderPicker(context),
903 color: colorScheme.primary,
904 textColor: colorScheme.onPrimary,
905 isDisabled: _swapButtonDisabled(),
906 isLoading: widget.exchangeViewModel.tradeState is TradeIsCreating,
907 ),
908 ),
909 ],
910 ),
911 ),
912 ],
913 ),
914 ),
915 ),
916 ),
917 ],
918 ),
919 ),
920 );
921 }
922
923 bool _swapButtonDisabled() {
924 if (widget.exchangeViewModel.selectedProviders.isEmpty) {
925 return true;
926 }
927
928 if (widget.exchangeViewModel.receiveAddress.isEmpty) {
929 return true;
930 }
931
932 if (widget.exchangeViewModel.status is! SyncedSyncStatus) {
933 return true;
934 }
935
936 if (widget.exchangeViewModel.depositAmount.isEmpty) {
937 return true;
938 }
939
940 return false;
941 }
942 }
943
944 class SwapProviderPreview extends StatelessWidget {
945 const SwapProviderPreview({
946 required this.exchangeViewModel,
947 super.key,
948 });
949
950 final ExchangeViewModel exchangeViewModel;
951
952 @override
953 Widget build(BuildContext context) => Observer(
954 builder: (_) {
955 final isFetchingDeposit =
956 exchangeViewModel.isFixedRateMode && exchangeViewModel.receiveAmount.isNotEmpty;
957 if (exchangeViewModel.depositAmount.isEmpty && !isFetchingDeposit) {
958 return const SizedBox.shrink();
959 }
960
961 final provider = exchangeViewModel.forcedProvider ?? exchangeViewModel.providerDisplay;
962 final rate = exchangeViewModel.forcedProvider == null
963 ? exchangeViewModel.bestRate
964 : exchangeViewModel.forcedProviderRate;
965
966 return GestureDetector(
967 onTap: () {
968 if (provider != null) {
969 Navigator.of(context).push(
970 CupertinoPageRoute(
971 builder: (context) => Material(
972 child: ProviderSelectorPage(exchangeViewModel: exchangeViewModel),
973 ),
974 ),
975 );
976 }
977 },
978 child: Container(
979 height: 48,
980 decoration: BoxDecoration(
981 color: Theme.of(context).colorScheme.surfaceContainer,
982 borderRadius: BorderRadius.circular(16),
983 ),
984 child: Padding(
985 padding: const EdgeInsets.symmetric(horizontal: 12),
986 child: Row(
987 mainAxisAlignment: MainAxisAlignment.spaceBetween,
988 children: [
989 Row(
990 spacing: 12,
991 children: [
992 if (provider != null)
993 CakeImageWidget(
994 imageUrl: provider.description.image,
995 width: 28,
996 height: 28,
997 ),
998 if (provider == null) const CupertinoActivityIndicator(),
999 Text(
1000 provider?.title ?? "${S.of(context).finding_provider}...",
1001 style: TextStyle(
1002 fontSize: 14,
1003 fontWeight: FontWeight.w400,
1004 color: provider == null
1005 ? Theme.of(context).colorScheme.onSurfaceVariant
1006 : Theme.of(context).colorScheme.onSurface,
1007 ),
1008 ),
1009 ],
1010 ),
1011 if (provider != null)
1012 Row(
1013 children: [
1014 Text(
1015 "1 ${exchangeViewModel.depositCurrency}${rate.toStringAsFixed(6)} ${exchangeViewModel.receiveCurrency}",
1016 style: TextStyle(
1017 fontSize: 14,
1018 fontWeight: FontWeight.w400,
1019 color: Theme.of(context).colorScheme.onSurfaceVariant,
1020 ),
1021 ),
1022 CakeImageWidget(
1023 imageUrl: "assets/new-ui/chooser.svg",
1024 colorFilter: ColorFilter.mode(
1025 Theme.of(context).colorScheme.onSurfaceVariant,
1026 BlendMode.srcIn,
1027 ),
1028 ),
1029 ],
1030 ),
1031 ],
1032 ),
1033 ),
1034 ),
1035 );
1036 },
1037 );
1038 }