dev
dart 803 lines 33.4 KB
Raw
1 import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart';
2 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
3 import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
4 import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
5 import 'package:cake_wallet/core/auth_service.dart';
6 import 'package:cake_wallet/di.dart';
7 import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart';
8 import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart';
9 import 'package:cake_wallet/src/widgets/add_template_button.dart';
10 import 'package:cake_wallet/utils/debounce.dart';
11 import 'package:cake_wallet/utils/payment_request.dart';
12 import 'package:cake_wallet/utils/responsive_layout_util.dart';
13 import 'package:cw_core/sync_status.dart';
14 import 'package:cw_core/utils/print_verbose.dart';
15 import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
16 import 'package:flutter/material.dart';
17 import 'package:flutter_mobx/flutter_mobx.dart';
18 import 'package:keyboard_actions/keyboard_actions.dart';
19 import 'package:mobx/mobx.dart';
20 import 'package:cake_wallet/exchange/exchange_template.dart';
21 import 'package:cake_wallet/exchange/exchange_trade_state.dart';
22 import 'package:cake_wallet/exchange/limits_state.dart';
23 import 'package:cake_wallet/src/screens/base_page.dart';
24 import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
25 import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
26 import 'package:cake_wallet/src/widgets/template_tile.dart';
27 import 'package:cake_wallet/src/widgets/trail_button.dart';
28 import 'package:cake_wallet/utils/show_pop_up.dart';
29 import 'package:cake_wallet/routes.dart';
30 import 'package:cake_wallet/generated/i18n.dart';
31 import 'package:cw_core/crypto_currency.dart';
32 import 'package:cake_wallet/src/screens/exchange/widgets/exchange_card.dart';
33 import 'package:cake_wallet/src/widgets/primary_button.dart';
34 import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart';
35 import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
36 import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart';
37 import 'package:cake_wallet/core/address_validator.dart';
38 import 'package:cake_wallet/core/amount_validator.dart';
39 import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart';
40 import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart';
41
42 class ExchangePage extends BasePage {
43 ExchangePage(
44 this.exchangeViewModel, this.authService, this.adrResService, this.initialPaymentRequest) {
45 depositWalletName = exchangeViewModel.depositCurrency == CryptoCurrency.xmr
46 ? exchangeViewModel.wallet.name
47 : null;
48 receiveWalletName = exchangeViewModel.receiveCurrency == CryptoCurrency.xmr
49 ? exchangeViewModel.wallet.name
50 : null;
51 }
52
53 final ExchangeViewModel exchangeViewModel;
54 final AuthService authService;
55 final AddressResolverService adrResService;
56 final PaymentRequest? initialPaymentRequest;
57 final depositKey = GlobalKey<ExchangeCardState>();
58 final receiveKey = GlobalKey<ExchangeCardState>();
59 final _formKey = GlobalKey<FormState>();
60 final _depositAmountFocus = FocusNode();
61 final _depositAddressFocus = FocusNode();
62 final _receiveAmountFocus = FocusNode();
63 final _receiveAddressFocus = FocusNode();
64 final _receiveAmountDebounce = Debounce(Duration(milliseconds: 500));
65 Debounce _depositAmountDebounce = Debounce(Duration(milliseconds: 500));
66 var _isReactionsSet = false;
67
68 late final String? depositWalletName;
69 late final String? receiveWalletName;
70
71 @override
72 String get title => S.current.exchange;
73
74 @override
75 bool get gradientBackground => true;
76
77 @override
78 bool get gradientAll => true;
79
80 @override
81 bool get resizeToAvoidBottomInset => false;
82
83 @override
84 bool get extendBodyBehindAppBar => true;
85
86 @override
87 AppBarStyle get appBarStyle => AppBarStyle.transparent;
88
89 @override
90 Function(BuildContext)? get pushToNextWidget => (context) {
91 FocusScopeNode currentFocus = FocusScope.of(context);
92 if (!currentFocus.hasPrimaryFocus) {
93 currentFocus.focusedChild?.unfocus();
94 }
95 };
96
97 bool get _shouldWaitTillSynced =>
98 [CryptoCurrency.xmr, CryptoCurrency.btc, CryptoCurrency.ltc]
99 .contains(exchangeViewModel.depositCurrency) &&
100 !(exchangeViewModel.status is SyncedSyncStatus);
101
102 @override
103 Widget middle(BuildContext context) => Row(
104 mainAxisAlignment: MainAxisAlignment.center,
105 children: [
106 Padding(
107 padding: const EdgeInsets.only(right: 6.0),
108 child: Observer(
109 builder: (_) =>
110 SyncIndicatorIcon(isSynced: exchangeViewModel.status is SyncedSyncStatus),
111 ),
112 ),
113 PresentProviderPicker(exchangeViewModel: exchangeViewModel)
114 ],
115 );
116
117 @override
118 Widget trailing(BuildContext context) => TrailButton(
119 caption: S.of(context).reset,
120 onPressed: () {
121 _formKey.currentState?.reset();
122 exchangeViewModel.reset();
123 },
124 );
125
126 @override
127 Widget? leading(BuildContext context) {
128 final _backButton = Icon(
129 Icons.arrow_back_ios,
130 color: Theme.of(context).colorScheme.primary,
131 size: 16,
132 );
133 final _closeButton = currentTheme.isDark ? closeButtonImageDarkTheme : closeButtonImage;
134
135 bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI;
136
137 return MergeSemantics(
138 child: SizedBox(
139 height: isMobileView ? 37 : 45,
140 width: isMobileView ? 37 : 45,
141 child: ButtonTheme(
142 minWidth: double.minPositive,
143 child: Semantics(
144 label: !isMobileView ? S.of(context).close : S.of(context).seed_alert_back,
145 child: TextButton(
146 style: ButtonStyle(
147 overlayColor: WidgetStateColor.resolveWith((states) => Colors.transparent),
148 ),
149 onPressed: () => onClose(context),
150 child: !isMobileView ? _closeButton : _backButton,
151 ),
152 ),
153 ),
154 ),
155 );
156 }
157
158 @override
159 Widget body(BuildContext context) {
160 WidgetsBinding.instance.addPostFrameCallback((_) => _setReactions(context, exchangeViewModel));
161
162 return KeyboardActions(
163 disableScroll: true,
164 config: KeyboardActionsConfig(
165 keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
166 keyboardBarColor: Theme.of(context).colorScheme.surfaceContainerHighest,
167 nextFocus: false,
168 actions: [
169 KeyboardActionsItem(
170 focusNode: _depositAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]),
171 KeyboardActionsItem(
172 focusNode: _receiveAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()])
173 ],
174 ),
175 child: Container(
176 color: Theme.of(context).colorScheme.surface,
177 child: Form(
178 key: _formKey,
179 child: ScrollableWithBottomSection(
180 contentPadding: EdgeInsets.only(bottom: 24),
181 content: Observer(
182 builder: (_) => Column(
183 children: <Widget>[
184 _exchangeCardsSection(context),
185 Padding(
186 padding: EdgeInsets.only(top: 12, left: 24),
187 child: Row(
188 mainAxisAlignment: MainAxisAlignment.start,
189 children: [
190 StandardCheckbox(
191 value: exchangeViewModel.isFixedRateMode,
192 caption: S.of(context).fixed_rate,
193 onChanged: (value) {
194 if (value) {
195 exchangeViewModel.enableFixedRateMode();
196 } else {
197 exchangeViewModel.isFixedRateMode = false;
198 }
199 },
200 ),
201 ],
202 ),
203 ),
204 SizedBox(height: 30),
205 _buildTemplateSection(context)
206 ],
207 ),
208 ),
209 bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
210 bottomSection: Column(
211 children: <Widget>[
212 Padding(
213 padding: EdgeInsets.only(bottom: 15),
214 child: Observer(
215 builder: (_) {
216 final description = exchangeViewModel.isFixedRateMode
217 ? exchangeViewModel.isAvailableInSelected
218 ? S.of(context).amount_is_guaranteed
219 : S.of(context).fixed_pair_not_supported
220 : exchangeViewModel.isAvailableInSelected
221 ? S.of(context).amount_is_estimate
222 : S.of(context).this_pair_is_not_supported_warning;
223 return Row(
224 children: [
225 if (description == S.of(context).this_pair_is_not_supported_warning)
226 Expanded(
227 child: Container(
228 alignment: Alignment.centerRight,
229 child: Icon(Icons.warning_amber_rounded,
230 color: Theme.of(context).colorScheme.error, size: 26),
231 ),
232 ),
233 Expanded(
234 flex: 8,
235 child: Text(
236 description,
237 textAlign: TextAlign.center,
238 softWrap: true,
239 overflow: TextOverflow.ellipsis,
240 maxLines: 3,
241 style: Theme.of(context).textTheme.bodySmall?.copyWith(
242 color: Theme.of(context).colorScheme.onSurfaceVariant,
243 fontWeight: FontWeight.w500,
244 ),
245 ),
246 ),
247 ],
248 );
249 },
250 ),
251 ),
252 Observer(
253 builder: (_) => LoadingPrimaryButton(
254 key: ValueKey('exchange_page_exchange_button_key'),
255 text: exchangeViewModel.isAvailableInSelected
256 ? S.of(context).swap
257 : S.of(context).change_selected_exchanges,
258 onPressed: exchangeViewModel.isAvailableInSelected
259 ? () {
260 FocusScope.of(context).unfocus();
261
262 if (_formKey.currentState != null &&
263 _formKey.currentState!.validate()) {
264 if (_shouldWaitTillSynced) {
265 showPopUp<void>(
266 context: context,
267 builder: (BuildContext context) {
268 return AlertWithOneAction(
269 alertTitle: S.of(context).exchange,
270 alertContent: S.of(context).exchange_sync_alert_content,
271 buttonText: S.of(context).ok,
272 buttonAction: () => Navigator.of(context).pop(),
273 );
274 },
275 );
276 } else {
277 final check = exchangeViewModel.shouldDisplayTOTP();
278 authService.authenticateAction(
279 context,
280 conditionToDetermineIfToUse2FA: check,
281 onAuthSuccess: (value) {
282 if (value) {
283 exchangeViewModel.createTrade();
284 }
285 },
286 );
287 }
288 }
289 }
290 : () => PresentProviderPicker(exchangeViewModel: exchangeViewModel)
291 .presentProviderPicker(context),
292 color: Theme.of(context).colorScheme.primary,
293 textColor: Theme.of(context).colorScheme.onPrimary,
294 isDisabled: exchangeViewModel.selectedProviders.isEmpty,
295 isLoading: exchangeViewModel.tradeState is TradeIsCreating,
296 ),
297 ),
298 ],
299 ),
300 ),
301 ),
302 ),
303 );
304 }
305
306 Widget _buildTemplateSection(BuildContext context) {
307 return Container(
308 height: 40,
309 width: double.infinity,
310 padding: EdgeInsets.only(left: 24),
311 child: SingleChildScrollView(
312 scrollDirection: Axis.horizontal,
313 child: Observer(
314 builder: (_) {
315 final templates = exchangeViewModel.templates;
316
317 return Row(
318 children: <Widget>[
319 AddTemplateButton(
320 onTap: () => Navigator.of(context).pushNamed(Routes.exchangeTemplate),
321 currentTemplatesLength: templates.length,
322 ),
323 ListView.builder(
324 scrollDirection: Axis.horizontal,
325 shrinkWrap: true,
326 physics: NeverScrollableScrollPhysics(),
327 itemCount: templates.length,
328 itemBuilder: (context, index) {
329 final template = templates[index];
330
331 return TemplateTile(
332 key: UniqueKey(),
333 amount: template.amount,
334 from: template.depositCurrencyTitle,
335 to: template.receiveCurrencyTitle,
336 onTap: () {
337 applyTemplate(context, exchangeViewModel, template);
338 },
339 onRemove: () {
340 showPopUp<void>(
341 context: context,
342 builder: (dialogContext) {
343 return AlertWithTwoActions(
344 alertTitle: S.of(context).template,
345 alertContent: S.of(context).confirm_delete_template,
346 rightButtonText: S.of(context).delete,
347 leftButtonText: S.of(context).cancel,
348 actionRightButton: () {
349 Navigator.of(dialogContext).pop();
350 exchangeViewModel.removeTemplate(template: template);
351 exchangeViewModel.updateTemplate();
352 },
353 actionLeftButton: () => Navigator.of(dialogContext).pop(),
354 );
355 },
356 );
357 },
358 );
359 },
360 ),
361 ],
362 );
363 },
364 ),
365 ),
366 );
367 }
368
369 void applyTemplate(
370 BuildContext context, ExchangeViewModel exchangeViewModel, ExchangeTemplate template) async {
371 final depositCryptoCurrency =
372 CryptoCurrency.safeParseCurrencyFromString(template.depositCurrency);
373 final receiveCryptoCurrency =
374 CryptoCurrency.safeParseCurrencyFromString(template.receiveCurrency);
375
376 if (depositCryptoCurrency == null || receiveCryptoCurrency == null) {
377 ///TO DO: add support for user tokens
378 return;
379 }
380
381 exchangeViewModel.changeDepositCurrency(currency: depositCryptoCurrency);
382 exchangeViewModel.changeReceiveCurrency(currency: receiveCryptoCurrency);
383
384 exchangeViewModel.changeDepositAmount(amount: template.amount);
385 exchangeViewModel.depositAddress = template.depositAddress;
386 exchangeViewModel.receiveAddress = template.receiveAddress;
387 exchangeViewModel.isReceiveAmountEntered = false;
388 exchangeViewModel.isFixedRateMode = false;
389
390 var domain = template.depositAddress;
391 exchangeViewModel.depositAddress =
392 await fetchParsedAddress(context, domain, depositCryptoCurrency);
393
394 domain = template.receiveAddress;
395 exchangeViewModel.receiveAddress =
396 await fetchParsedAddress(context, domain, receiveCryptoCurrency);
397 }
398
399 void _setReactions(BuildContext context, ExchangeViewModel exchangeViewModel) {
400 if (_isReactionsSet) {
401 return;
402 }
403
404 if (exchangeViewModel.feesViewModel.isLowFee) {
405 _showFeeAlert(context);
406 }
407
408 final depositAddressController = depositKey.currentState!.addressController;
409 final depositAmountController = depositKey.currentState!.amountController;
410 final receiveAddressController = receiveKey.currentState!.addressController;
411 final receiveAmountController = receiveKey.currentState!.amountController;
412 final limitsState = exchangeViewModel.limitsState;
413
414 if (limitsState is LimitsLoadedSuccessfully) {
415 final min = limitsState.limits.min != null ? limitsState.limits.min.toString() : null;
416 final max = limitsState.limits.max != null ? limitsState.limits.max.toString() : null;
417 final key = exchangeViewModel.isFixedRateMode ? receiveKey : depositKey;
418 key.currentState!.changeLimits(min: min, max: max);
419 }
420
421 _onCurrencyChange(exchangeViewModel.receiveCurrency, exchangeViewModel, receiveKey);
422 _onCurrencyChange(exchangeViewModel.depositCurrency, exchangeViewModel, depositKey);
423
424 reaction(
425 (_) => exchangeViewModel.wallet.name,
426 (_) =>
427 _onWalletNameChange(exchangeViewModel, exchangeViewModel.receiveCurrency, receiveKey));
428
429 reaction(
430 (_) => exchangeViewModel.wallet.name,
431 (_) =>
432 _onWalletNameChange(exchangeViewModel, exchangeViewModel.depositCurrency, depositKey));
433
434 reaction((_) => exchangeViewModel.receiveCurrency,
435 (CryptoCurrency currency) => _onCurrencyChange(currency, exchangeViewModel, receiveKey));
436
437 reaction((_) => exchangeViewModel.depositCurrency,
438 (CryptoCurrency currency) => _onCurrencyChange(currency, exchangeViewModel, depositKey));
439
440 reaction((_) => exchangeViewModel.depositAmount, (String amount) {
441 if (exchangeViewModel.isSendAllEnabled) {
442 depositAmountController.text = S.of(context).all;
443 } else if (depositAmountController.text != amount && amount != S.of(context).all) {
444 depositAmountController.text = amount;
445 }
446 });
447
448 reaction((_) => exchangeViewModel.depositAddress, (String address) {
449 if (depositKey.currentState!.addressController.text != address) {
450 depositKey.currentState!.addressController.text = address;
451 }
452 });
453
454 reaction((_) => exchangeViewModel.isDepositAddressEnabled, (bool isEnabled) {
455 depositKey.currentState!.isAddressEditable(isEditable: isEnabled);
456 });
457
458 reaction((_) => exchangeViewModel.receiveAmount, (String amount) {
459 if (receiveKey.currentState!.amountController.text != amount) {
460 receiveKey.currentState!.amountController.text = amount;
461 }
462 });
463
464 reaction((_) => exchangeViewModel.receiveAddress, (String address) {
465 if (receiveKey.currentState!.addressController.text != address) {
466 receiveKey.currentState!.addressController.text = address;
467 }
468 });
469
470 reaction((_) => exchangeViewModel.isReceiveAmountEditable, (bool isReceiveAmountEditable) {
471 receiveKey.currentState!.isAmountEditable(isEditable: isReceiveAmountEditable);
472 });
473
474 reaction((_) => exchangeViewModel.tradeState, (ExchangeTradeState state) {
475 if (state is TradeIsCreatedFailure) {
476 WidgetsBinding.instance.addPostFrameCallback((_) {
477 showPopUp<void>(
478 context: context,
479 builder: (BuildContext context) {
480 return AlertWithOneAction(
481 key: ValueKey('exchange_page_trade_creation_failure_dialog_key'),
482 buttonKey: ValueKey('exchange_page_trade_creation_failure_dialog_button_key'),
483 alertTitle: S.of(context).provider_error(state.title),
484 alertContent: state.error,
485 buttonText: S.of(context).ok,
486 buttonAction: () => Navigator.of(context).pop());
487 });
488 });
489 }
490 if (state is TradeIsCreatedSuccessfully) {
491 exchangeViewModel.reset();
492 (exchangeViewModel.tradesStore.trade?.provider == ExchangeProviderDescription.thorChain ||
493 exchangeViewModel.tradesStore.trade?.provider ==
494 ExchangeProviderDescription.chainflip)
495 ? Navigator.of(context).pushReplacementNamed(Routes.exchangeTrade)
496 : Navigator.of(context).pushReplacementNamed(Routes.exchangeConfirm);
497 }
498 });
499
500 reaction((_) => exchangeViewModel.limitsState, (LimitsState state) {
501 String? min;
502 String? max;
503
504 if (state is LimitsLoadedSuccessfully) {
505 min = state.limits.min != null ? state.limits.min.toString() : null;
506 max = state.limits.max != null ? state.limits.max.toString() : null;
507 }
508
509 if (state is LimitsLoadedFailure) {
510 min = '0';
511 max = '0';
512 }
513
514 if (state is LimitsIsLoading) {
515 min = '...';
516 max = '...';
517 }
518
519 if (exchangeViewModel.isFixedRateMode) {
520 depositKey.currentState!.changeLimits(min: null, max: null);
521 receiveKey.currentState!.changeLimits(min: min, max: max);
522 } else {
523 depositKey.currentState!.changeLimits(min: min, max: max);
524 receiveKey.currentState!.changeLimits(min: null, max: null);
525 }
526 });
527
528 reaction((_) => exchangeViewModel.bestRate, (double rate) {
529 if (exchangeViewModel.isFixedRateMode) {
530 exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text);
531 } else {
532 if (depositAmountController.text == S.current.all)
533 exchangeViewModel.changeDepositAmount(amount: exchangeViewModel.depositAmount);
534 else
535 exchangeViewModel.changeDepositAmount(amount: depositAmountController.text);
536 }
537 });
538
539 depositAddressController
540 .addListener(() => exchangeViewModel.depositAddress = depositAddressController.text);
541
542 depositAmountController.addListener(() {
543 if (depositAmountController.text != exchangeViewModel.depositAmount &&
544 depositAmountController.text != S.of(context).all) {
545 exchangeViewModel.isSendAllEnabled = false;
546 final isThorChain = exchangeViewModel.selectedProviders
547 .any((provider) => provider is ThorChainExchangeProvider);
548 final isChainflip = exchangeViewModel.selectedProviders
549 .any((provider) => provider is ChainflipExchangeProvider);
550
551 _depositAmountDebounce = isThorChain || isChainflip
552 ? Debounce(Duration(milliseconds: 1000))
553 : Debounce(Duration(milliseconds: 500));
554
555 _depositAmountDebounce.run(() {
556 exchangeViewModel.calculateBestRate();
557 exchangeViewModel.changeDepositAmount(amount: depositAmountController.text);
558 exchangeViewModel.isReceiveAmountEntered = false;
559 });
560 }
561 });
562
563 receiveAddressController
564 .addListener(() => exchangeViewModel.receiveAddress = receiveAddressController.text);
565
566 receiveAmountController.addListener(() {
567 if (receiveAmountController.text != exchangeViewModel.receiveAmount) {
568 _receiveAmountDebounce.run(() {
569 exchangeViewModel.calculateBestRate();
570 exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text);
571 exchangeViewModel.isReceiveAmountEntered = true;
572 });
573 }
574 });
575
576 reaction((_) => exchangeViewModel.wallet.walletAddresses.addressForExchange, (String address) {
577 if (exchangeViewModel.depositCurrency == CryptoCurrency.xmr) {
578 depositKey.currentState!.changeAddress(address: address);
579 }
580
581 if (exchangeViewModel.receiveCurrency == CryptoCurrency.xmr) {
582 receiveKey.currentState!.changeAddress(address: address);
583 }
584 });
585
586 _depositAddressFocus.addListener(() async {
587 if (!_depositAddressFocus.hasFocus && depositAddressController.text.isNotEmpty) {
588 final domain = depositAddressController.text;
589 exchangeViewModel.depositAddress =
590 await fetchParsedAddress(context, domain, exchangeViewModel.depositCurrency);
591 }
592 });
593
594 _receiveAddressFocus.addListener(() async {
595 if (!_receiveAddressFocus.hasFocus && receiveAddressController.text.isNotEmpty) {
596 final domain = receiveAddressController.text;
597 exchangeViewModel.receiveAddress =
598 await fetchParsedAddress(context, domain, exchangeViewModel.receiveCurrency);
599 }
600 });
601
602 _receiveAmountFocus.addListener(() {
603 if (_receiveAmountFocus.hasFocus) {
604 exchangeViewModel.enableFixedRateMode();
605 }
606 // exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text);
607 });
608
609 _depositAmountFocus.addListener(() {
610 exchangeViewModel.isFixedRateMode = false;
611 // exchangeViewModel.changeDepositAmount(
612 // amount: depositAmountController.text);
613 });
614
615 if (initialPaymentRequest != null) {
616 try {
617 exchangeViewModel.receiveCurrency =
618 CryptoCurrency.fromString(initialPaymentRequest!.scheme);
619 exchangeViewModel.setCanonicalReceiveAmount(initialPaymentRequest!.amount);
620 exchangeViewModel.receiveAddress = initialPaymentRequest!.address;
621 } catch (e) {
622 printV('error: ${e.toString()}');
623 // TODO
624 }
625 }
626
627 _isReactionsSet = true;
628 }
629
630 void _onCurrencyChange(CryptoCurrency currency, ExchangeViewModel exchangeViewModel,
631 GlobalKey<ExchangeCardState> key) {
632 final isCurrentTypeWallet = exchangeViewModel.useSameWalletAddress(currency);
633
634 key.currentState!.changeSelectedCurrency(currency);
635 key.currentState!.changeWalletName(isCurrentTypeWallet ? exchangeViewModel.wallet.name : '');
636
637 key.currentState!.changeAddress(
638 address:
639 isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.addressForExchange : '');
640 }
641
642 void _onWalletNameChange(ExchangeViewModel exchangeViewModel, CryptoCurrency currency,
643 GlobalKey<ExchangeCardState> key) {
644 final isCurrentTypeWallet = currency == exchangeViewModel.wallet.currency;
645
646 if (isCurrentTypeWallet) {
647 key.currentState!.changeWalletName(exchangeViewModel.wallet.name);
648 key.currentState!.addressController.text =
649 exchangeViewModel.wallet.walletAddresses.addressForExchange;
650 } else if (key.currentState!.addressController.text ==
651 exchangeViewModel.wallet.walletAddresses.addressForExchange) {
652 key.currentState!.changeWalletName('');
653 key.currentState!.addressController.text = '';
654 }
655 }
656
657 Future<String> fetchParsedAddress(
658 BuildContext context, String domain, CryptoCurrency currency) async {
659 final parsedAddresses = await adrResService.resolve(
660 query: domain, wallet: exchangeViewModel.wallet, currency: currency);
661 return parsedAddresses.isNotEmpty
662 ? (parsedAddresses.first.parsedAddressByCurrencyMap[currency] ?? domain)
663 : domain;
664 }
665
666 void _showFeeAlert(BuildContext context) async {
667 await Future<void>.delayed(Duration(seconds: 1));
668 final confirmed = await showPopUp<bool>(
669 context: context,
670 builder: (dialogContext) {
671 return AlertWithTwoActions(
672 alertTitle: S.of(context).low_fee,
673 alertContent: S.of(context).low_fee_alert,
674 leftButtonText: S.of(context).ignor,
675 rightButtonText: S.of(context).use_suggested,
676 actionLeftButton: () => Navigator.of(dialogContext).pop(false),
677 actionRightButton: () => Navigator.of(dialogContext).pop(true));
678 }) ??
679 false;
680 if (confirmed) {
681 exchangeViewModel.feesViewModel.setDefaultTransactionPriority();
682 }
683 }
684
685 void disposeBestRateSync() => exchangeViewModel.bestRateSync.cancel();
686
687 Widget _exchangeCardsSection(BuildContext context) {
688 final firstExchangeCard = Observer(
689 builder: (_) => ExchangeCard(
690 cardInstanceName: 'deposit_exchange_card',
691 onDispose: disposeBestRateSync,
692 hasAllAmount: exchangeViewModel.hasAllAmount,
693 allAmount:
694 exchangeViewModel.hasAllAmount ? () => exchangeViewModel.enableSendAllAmount() : null,
695 isAllAmountEnabled: exchangeViewModel.isSendAllEnabled,
696 amountFocusNode: _depositAmountFocus,
697 addressFocusNode: _depositAddressFocus,
698 key: depositKey,
699 title: S.of(context).you_will_send,
700 initialCurrency: exchangeViewModel.depositCurrency,
701 initialWalletName: depositWalletName ?? '',
702 initialAddress: exchangeViewModel.depositCurrency == exchangeViewModel.wallet.currency
703 ? exchangeViewModel.wallet.walletAddresses.addressForExchange
704 : exchangeViewModel.depositAddress,
705 initialIsAmountEditable: true,
706 initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled,
707 isAmountEstimated: false,
708 hasRefundAddress: true,
709 isMoneroWallet: exchangeViewModel.isMoneroWallet,
710 currencies: exchangeViewModel.depositCurrencies,
711 onCurrencySelected: (currency) {
712 exchangeViewModel.changeDepositCurrency(currency: currency);
713 },
714 currencyButtonColor: Colors.transparent,
715 addressButtonsColor: Colors.transparent,
716 borderColor: Theme.of(context).colorScheme.outlineVariant,
717 fillColor: Theme.of(context).colorScheme.surfaceContainer,
718 currencyValueValidator: (value) {
719 return !exchangeViewModel.isFixedRateMode && value != S.of(context).all
720 ? AmountValidator(
721 isAutovalidate: true,
722 currency: exchangeViewModel.depositCurrency,
723 amountParsingProxy: exchangeViewModel.amountParsingProxy,
724 minValue: exchangeViewModel.limits.min.toString(),
725 maxValue: exchangeViewModel.limits.max.toString(),
726 ).call(value)
727 : null;
728 },
729 addressTextFieldValidator: AddressValidator(type: exchangeViewModel.depositCurrency),
730 onPushPasteButton: (context) async {
731 final domain = exchangeViewModel.depositAddress;
732 exchangeViewModel.depositAddress =
733 await fetchParsedAddress(context, domain, exchangeViewModel.depositCurrency);
734 },
735 onPushAddressBookButton: (context) async {
736 final domain = exchangeViewModel.depositAddress;
737 exchangeViewModel.depositAddress =
738 await fetchParsedAddress(context, domain, exchangeViewModel.depositCurrency);
739 },
740 useSatoshis: exchangeViewModel.useDepositBaseUnit,
741 ),
742 );
743
744 final secondExchangeCard = Observer(
745 builder: (_) => ExchangeCard(
746 cardInstanceName: 'receive_exchange_card',
747 onDispose: disposeBestRateSync,
748 amountFocusNode: _receiveAmountFocus,
749 addressFocusNode: _receiveAddressFocus,
750 key: receiveKey,
751 title: S.of(context).you_will_get,
752 initialCurrency: exchangeViewModel.receiveCurrency,
753 initialWalletName: receiveWalletName ?? '',
754 initialAddress: exchangeViewModel.receiveCurrency == exchangeViewModel.wallet.currency
755 ? exchangeViewModel.wallet.walletAddresses.addressForExchange
756 : exchangeViewModel.receiveAddress,
757 initialIsAmountEditable: exchangeViewModel.isReceiveAmountEditable,
758 isAmountEstimated: true,
759 isMoneroWallet: exchangeViewModel.isMoneroWallet,
760 currencies: exchangeViewModel.receiveCurrencies,
761 onCurrencySelected: (currency) =>
762 exchangeViewModel.changeReceiveCurrency(currency: currency),
763 currencyButtonColor: Colors.transparent,
764 addressButtonsColor: Colors.transparent,
765 borderColor: Theme.of(context).colorScheme.outlineVariant,
766 fillColor: Theme.of(context).colorScheme.surfaceContainerLow,
767 currencyValueValidator: (value) {
768 return exchangeViewModel.isFixedRateMode
769 ? AmountValidator(
770 isAutovalidate: true,
771 currency: exchangeViewModel.receiveCurrency,
772 amountParsingProxy: exchangeViewModel.amountParsingProxy,
773 minValue: exchangeViewModel.limits.min.toString(),
774 maxValue: exchangeViewModel.limits.max.toString(),
775 ).call(value)
776 : null;
777 },
778 addressTextFieldValidator: AddressValidator(type: exchangeViewModel.receiveCurrency),
779 onPushPasteButton: (context) async {
780 final domain = exchangeViewModel.receiveAddress;
781 exchangeViewModel.receiveAddress =
782 await fetchParsedAddress(context, domain, exchangeViewModel.receiveCurrency);
783 },
784 onPushAddressBookButton: (context) async {
785 final domain = exchangeViewModel.receiveAddress;
786 exchangeViewModel.receiveAddress =
787 await fetchParsedAddress(context, domain, exchangeViewModel.receiveCurrency);
788 },
789 useSatoshis: exchangeViewModel.useReceiveBaseUnit,
790 ),
791 );
792
793 return responsiveLayoutUtil.shouldRenderMobileUI
794 ? MobileExchangeCardsSection(
795 firstExchangeCard: firstExchangeCard,
796 secondExchangeCard: secondExchangeCard,
797 )
798 : DesktopExchangeCardsSection(
799 firstExchangeCard: firstExchangeCard,
800 secondExchangeCard: secondExchangeCard,
801 );
802 }
803 }