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/entities/qr_scanner.dart';
7
-import 'package:cake_wallet/exchange/exchange_trade_state.dart';
8
-import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
9
-import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
10
-import 'package:cake_wallet/generated/i18n.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/fiat_amount_bar.dart';
15
-import 'package:cake_wallet/new-ui/widgets/send_page/send_memo_input.dart';
16
-import 'package:cake_wallet/new-ui/widgets/send_page/send_syncing_indicator.dart';
17
-import 'package:cake_wallet/new-ui/widgets/swap_page/provider_selector_page.dart';
18
-import 'package:cake_wallet/new-ui/widgets/swap_page/refund_address_modal.dart';
19
-import 'package:cake_wallet/new-ui/widgets/swap_page/swap_address_selection_modal.dart';
20
-import 'package:cake_wallet/new-ui/widgets/swap_page/swap_confirm_sheet.dart';
21
-import 'package:cake_wallet/new-ui/widgets/swap_page/swap_limit_popup.dart';
22
-import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart';
23
-import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart';
24
-import 'package:cake_wallet/new-ui/widgets/swap_page/swap_options_page.dart';
25
-import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart';
26
-import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
27
-import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
28
-import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
29
-import 'package:cake_wallet/src/widgets/primary_button.dart';
30
-import 'package:cake_wallet/utils/debounce.dart';
31
-import 'package:cake_wallet/utils/decimal_input_formatter.dart';
32
-import 'package:cake_wallet/utils/payment_request.dart';
33
-import 'package:cake_wallet/utils/permission_handler.dart';
34
-import 'package:cw_core/wallet_info.dart';
35
-import 'package:flutter/services.dart';
36
-import 'package:permission_handler/permission_handler.dart';
37
-import 'package:cake_wallet/utils/show_pop_up.dart';
38
-import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart';
39
-import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart';
40
-import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart';
41
-import 'package:cw_core/crypto_currency.dart';
42
-import 'package:cw_core/currencies_with_memo.dart';
43
-import 'package:cw_core/currency.dart';
44
-import 'package:cw_core/sync_status.dart';
45
-import 'package:cw_core/utils/print_verbose.dart';
46
-import 'package:cw_core/wallet_type.dart';
47
-import 'package:flutter/cupertino.dart';
48
-import 'package:flutter/material.dart';
49
-import 'package:flutter_mobx/flutter_mobx.dart';
50
-import 'package:mobx/mobx.dart';
51
-import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
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/keyboard_hide_overlay.dart";
11
+import "package:cake_wallet/new-ui/widgets/modern_button.dart";
12
+import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
13
+import "package:cake_wallet/new-ui/widgets/send_page/send_syncing_indicator.dart";
14
+import "package:cake_wallet/new-ui/widgets/swap_page/anypay_swap_footer.dart";
15
+import "package:cake_wallet/new-ui/widgets/swap_page/provider_selector_page.dart";
16
+import "package:cake_wallet/new-ui/widgets/swap_page/swap_amount_box.dart";
17
+import "package:cake_wallet/new-ui/widgets/swap_page/swap_confirm_sheet.dart";
18
+import "package:cake_wallet/new-ui/widgets/swap_page/swap_from_send_args.dart";
19
+import "package:cake_wallet/new-ui/widgets/swap_page/swap_limit_popup.dart";
20
+import "package:cake_wallet/new-ui/widgets/swap_page/swap_options_page.dart";
21
+import "package:cake_wallet/new-ui/widgets/swap_page/swap_section_header.dart";
22
+import "package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart";
23
+import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
24
+import "package:cake_wallet/src/widgets/alert_with_two_actions.dart";
25
+import "package:cake_wallet/src/widgets/cake_image_widget.dart";
26
+import "package:cake_wallet/src/widgets/primary_button.dart";
27
+import "package:cake_wallet/utils/debounce.dart";
28
+import "package:cake_wallet/utils/payment_request.dart";
29
+import "package:cake_wallet/utils/show_pop_up.dart";
30
+import "package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart";
31
+import "package:cake_wallet/view_model/exchange/exchange_view_model.dart";
32
+import "package:cake_wallet/view_model/wallet_switcher_view_model.dart";
33
+import "package:cw_core/crypto_currency.dart";
34
+import "package:cw_core/currency_for_wallet_type.dart";
35
+import "package:cw_core/sync_status.dart";
36
+import "package:cw_core/utils/print_verbose.dart";
37
+import "package:cw_core/wallet_type.dart";
38
+import "package:flutter/cupertino.dart";
39
+import "package:flutter/material.dart";
40
+import "package:flutter/services.dart";
41
+import "package:flutter_mobx/flutter_mobx.dart";
42
+import "package:mobx/mobx.dart";
43
+import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
44
45
class NewSwapPage extends StatefulWidget {
46
NewSwapPage(
55
- this.exchangeViewModel, this.authService, this.adrResService, this.initialPaymentRequest,
56
- {required this.walletSwitcherViewModel, CryptoCurrency? initialCurrency}) {
47
+ this.exchangeViewModel,
48
+ this.authService,
49
+ this.adrResService,
50
+ this.initialPaymentRequest, {
51
+ required this.walletSwitcherViewModel,
52
+ CryptoCurrency? initialCurrency,
53
+ this.fromSend,
54
+ }) {
55
depositWalletName = exchangeViewModel.depositCurrency == CryptoCurrency.xmr
56
? exchangeViewModel.wallet.name
57
: null;
68
final AuthService authService;
69
final AddressResolverService adrResService;
70
final PaymentRequest? initialPaymentRequest;
71
+ final SwapFromSendArgs? fromSend;
72
late final String? depositWalletName;
73
late final String? receiveWalletName;
74
84
final _depositAddressFocus = FocusNode();
85
final _receiveAmountFocus = FocusNode();
86
final _receiveAddressFocus = FocusNode();
88
- final _receiveAmountDebounce = Debounce(Duration(milliseconds: 500));
89
- Debounce _depositAmountDebounce = Debounce(Duration(milliseconds: 500));
87
+ final _receiveAmountDebounce = Debounce(const Duration(milliseconds: 500));
88
+ Debounce _depositAmountDebounce = Debounce(const Duration(milliseconds: 500));
89
+ final List<ReactionDisposer> _disposers = [];
90
91
bool get _shouldWaitTillSynced =>
92
[CryptoCurrency.xmr, CryptoCurrency.btc, CryptoCurrency.ltc]
101
}
102
103
WidgetsBinding.instance.addPostFrameCallback((_) {
104
+ if (!mounted) {
105
+ return;
106
+ }
107
+
108
// if (!widget.exchangeViewModel.decentralizedExchangesPromptDismissed) {
109
// showMaterialModalBottomSheet(
110
// context: context,
133
}
134
widget.exchangeViewModel.isFixedRateMode = false;
135
widget.exchangeViewModel.isSendAllEnabled = false;
132
- Future.delayed(Duration(milliseconds: 200)).then((_) {
136
+ Future.delayed(const Duration(milliseconds: 200)).then((_) {
137
+ if (!mounted) {
138
+ return;
139
+ }
140
if (double.tryParse(depositFiatAmountController.text) != null) {
141
widget.exchangeViewModel
142
.setDepositAmountFromFiat(fiatAmount: depositFiatAmountController.text);
145
});
146
});
147
receiveFiatAmountController.addListener(() {
141
- if (!receiveKey.currentState!.amountFocusNode.hasFocus) return;
148
+ if (!receiveKey.currentState!.amountFocusNode.hasFocus) {
149
+ return;
150
+ }
151
152
widget.exchangeViewModel.enableFixedRateMode();
153
widget.exchangeViewModel.isSendAllEnabled = false;
145
- Future.delayed(Duration(milliseconds: 200)).then((_) {
154
+ Future.delayed(const Duration(milliseconds: 200)).then((_) {
155
+ if (!mounted) {
156
+ return;
157
+ }
158
if (double.tryParse(receiveFiatAmountController.text) != null) {
159
String text = receiveFiatAmountController.text;
160
if (text.contains(".")) {
149
- text = text.replaceAll(RegExp(r'0+$'), '');
150
- text = text.replaceAll(RegExp(r'\.$'), '');
161
+ text = text.replaceAll(RegExp(r"0+$"), "");
162
+ text = text.replaceAll(RegExp(r"\.$"), "");
163
}
164
widget.exchangeViewModel
165
.setReceiveAmountFromFiat(fiatAmount: receiveFiatAmountController.text);
167
}
168
});
169
});
158
- reaction((_) => widget.exchangeViewModel.isFixedRateMode, (val) {
159
- Future.delayed(Duration(seconds: 3)).then((_) {
170
+ _disposers.add(reaction((_) => widget.exchangeViewModel.isFixedRateMode, (val) {
171
+ Future.delayed(const Duration(seconds: 3)).then((_) {
172
if (val) {
161
- if (depositKey.currentState?.mounted == true)
173
+ if (depositKey.currentState?.mounted == true) {
174
depositKey.currentState!.updateFiatAmount();
175
+ }
176
} else {
164
- if (receiveKey.currentState?.mounted == true)
177
+ if (receiveKey.currentState?.mounted == true) {
178
receiveKey.currentState!.updateFiatAmount();
179
+ }
180
}
181
});
168
- });
182
+ }));
183
170
- reaction((_) => widget.exchangeViewModel.depositAmount, (String amount) {
171
- if (depositKey.currentState!.amountFocusNode.hasFocus) return;
184
+ _disposers.add(reaction((_) => widget.exchangeViewModel.depositAmount, (String amount) {
185
+ if (depositKey.currentState!.amountFocusNode.hasFocus) {
186
+ return;
187
+ }
188
189
if (widget.exchangeViewModel.isSendAllEnabled) {
190
depositAmountController.text = S.of(context).all;
191
} else if (depositAmountController.text != amount && amount != S.of(context).all) {
192
depositAmountController.text = amount;
193
}
178
- });
194
+ }));
195
196
_onCurrencyChange(
181
- widget.exchangeViewModel.receiveCurrency, widget.exchangeViewModel, receiveKey);
197
+ widget.exchangeViewModel.receiveCurrency,
198
+ widget.exchangeViewModel,
199
+ receiveKey,
200
+ );
201
_onCurrencyChange(
183
- widget.exchangeViewModel.depositCurrency, widget.exchangeViewModel, depositKey);
184
-
185
- reaction(
186
- (_) => widget.exchangeViewModel.wallet.name,
187
- (_) => _onWalletNameChange(
188
- widget.exchangeViewModel, widget.exchangeViewModel.receiveCurrency, receiveKey));
202
+ widget.exchangeViewModel.depositCurrency,
203
+ widget.exchangeViewModel,
204
+ depositKey,
205
+ );
206
190
- reaction(
191
- (_) => widget.exchangeViewModel.wallet.name,
192
- (_) => _onWalletNameChange(
193
- widget.exchangeViewModel, widget.exchangeViewModel.depositCurrency, depositKey));
207
+ _disposers.add(reaction(
208
+ (_) => widget.exchangeViewModel.wallet.name,
209
+ (_) => _onWalletNameChange(
210
+ widget.exchangeViewModel,
211
+ widget.exchangeViewModel.receiveCurrency,
212
+ receiveKey,
213
+ ),
214
+ ));
215
+
216
+ _disposers.add(reaction(
217
+ (_) => widget.exchangeViewModel.wallet.name,
218
+ (_) => _onWalletNameChange(
219
+ widget.exchangeViewModel,
220
+ widget.exchangeViewModel.depositCurrency,
221
+ depositKey,
222
+ ),
223
+ ));
224
195
- reaction(
225
+ _disposers.add(reaction(
226
(_) => widget.exchangeViewModel.receiveCurrency,
227
(currency) => _onCurrencyChange(currency, widget.exchangeViewModel, receiveKey),
198
- );
228
+ ));
229
200
- reaction(
230
+ _disposers.add(reaction(
231
(_) => widget.exchangeViewModel.depositCurrency,
232
(currency) => _onCurrencyChange(currency, widget.exchangeViewModel, depositKey),
203
- );
233
+ ));
234
205
- reaction((_) => widget.exchangeViewModel.depositAddress, (String address) {
235
+ _disposers.add(reaction((_) => widget.exchangeViewModel.depositAddress, (String address) {
236
if (depositKey.currentState!.addressController.text != address) {
237
depositKey.currentState!.addressController.text = address;
238
}
209
- });
239
+ }));
240
211
- reaction((_) => widget.exchangeViewModel.isDepositAddressEnabled, (isEnabled) {});
241
+ _disposers
242
+ .add(reaction((_) => widget.exchangeViewModel.isDepositAddressEnabled, (isEnabled) {}));
243
213
- reaction((_) => widget.exchangeViewModel.receiveAmount, (String amount) {
244
+ _disposers.add(reaction((_) => widget.exchangeViewModel.receiveAmount, (String amount) {
245
if (receiveKey.currentState!.amountController.text != amount) {
246
receiveKey.currentState!.amountController.text = amount;
247
}
217
- });
248
+ }));
249
219
- reaction((_) => widget.exchangeViewModel.receiveAddress, (String address) {
250
+ _disposers.add(reaction((_) => widget.exchangeViewModel.receiveAddress, (String address) {
251
if (receiveKey.currentState!.addressController.text != address) {
252
receiveKey.currentState!.addressController.text = address;
253
}
223
- });
254
+ }));
255
225
- reaction((_) => widget.exchangeViewModel.isReceiveAmountEditable,
226
- (bool isReceiveAmountEditable) {});
256
+ _disposers.add(reaction(
257
+ (_) => widget.exchangeViewModel.isReceiveAmountEditable,
258
+ (bool isReceiveAmountEditable) {},
259
+ ));
260
228
- reaction((_) => widget.exchangeViewModel.tradeState, (ExchangeTradeState state) {
261
+ _disposers
262
+ .add(reaction((_) => widget.exchangeViewModel.tradeState, (ExchangeTradeState state) {
263
if (state is TradeIsCreatedFailure) {
264
WidgetsBinding.instance.addPostFrameCallback((_) {
265
if (mounted) {
266
showPopUp<void>(
233
- context: context,
234
- builder: (BuildContext context) {
235
- return AlertWithOneAction(
236
- key: ValueKey('exchange_page_trade_creation_failure_dialog_key'),
237
- buttonKey:
238
- ValueKey('exchange_page_trade_creation_failure_dialog_button_key'),
239
- alertTitle: S.of(context).provider_error(state.title),
240
- alertContent: state.error,
241
- buttonText: S.of(context).ok,
242
- buttonAction: () => Navigator.of(context).pop());
243
- });
267
+ context: context,
268
+ builder: (BuildContext context) => AlertWithOneAction(
269
+ key: const ValueKey("exchange_page_trade_creation_failure_dialog_key"),
270
+ buttonKey:
271
+ const ValueKey("exchange_page_trade_creation_failure_dialog_button_key"),
272
+ alertTitle: S.of(context).provider_error(state.title),
273
+ alertContent: state.error,
274
+ buttonText: S.of(context).ok,
275
+ buttonAction: () => Navigator.of(context).pop(),
276
+ ),
277
+ );
278
}
279
});
280
}
281
if (state is TradeIsCreatedSuccessfully) {
248
- if (widget.exchangeViewModel.tradeStarted) return;
282
+ if (widget.exchangeViewModel.tradeStarted) {
283
+ return;
284
+ }
285
final receiveAmount = widget.exchangeViewModel.receiveAmount;
286
// FIXME we don't know why a reset is/was needed here, it messes up ui so i removed it
287
// widget.exchangeViewModel.reset();
299
receiveAmount: receiveAmount,
300
);
301
showMaterialModalBottomSheet(
266
- context: context, builder: (context) => page, backgroundColor: Colors.transparent);
302
+ context: context,
303
+ builder: (context) => page,
304
+ backgroundColor: Colors.transparent,
305
+ );
306
}
268
- });
307
+ }));
308
270
- reaction((_) => widget.exchangeViewModel.bestRate, (double rate) {
309
+ _disposers.add(reaction((_) => widget.exchangeViewModel.bestRate, (double rate) {
310
if (widget.exchangeViewModel.isFixedRateMode) {
311
widget.exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text);
312
} else {
313
printV("bestrate");
275
- if ((depositKey.currentState?._fiatInputMode ?? false) ||
314
+ if ((depositKey.currentState?.fiatInputMode ?? false) ||
315
depositAmountController.text.isEmpty ||
277
- depositAmountController.text == S.current.all)
316
+ depositAmountController.text == S.current.all) {
317
widget.exchangeViewModel
318
.changeDepositAmount(amount: widget.exchangeViewModel.depositAmount);
280
- else
319
+ } else {
320
widget.exchangeViewModel.changeDepositAmount(amount: depositAmountController.text);
321
+ }
322
}
283
- });
323
+ }));
324
+
325
+ _disposers.add(reaction((_) => widget.exchangeViewModel.forcedProviderRate, (double rate) {
326
+ if (widget.exchangeViewModel.forcedProvider != null && rate == 0) {
327
+ return;
328
+ }
329
+ if (widget.exchangeViewModel.isFixedRateMode) {
330
+ widget.exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text);
331
+ } else if (depositAmountController.text.isNotEmpty &&
332
+ depositAmountController.text != S.current.all) {
333
+ widget.exchangeViewModel.changeDepositAmount(amount: depositAmountController.text);
334
+ }
335
+ }));
336
337
depositAddressController.addListener(
286
- () => widget.exchangeViewModel.depositAddress = depositAddressController.text);
338
+ () => widget.exchangeViewModel.depositAddress = depositAddressController.text,
339
+ );
340
341
depositAmountController.addListener(() {
342
if (depositAmountController.text != widget.exchangeViewModel.depositAmount &&
348
.any((provider) => provider is ChainflipExchangeProvider);
349
350
_depositAmountDebounce = isThorChain || isChainflip
298
- ? Debounce(Duration(milliseconds: 1000))
299
- : Debounce(Duration(milliseconds: 500));
351
+ ? Debounce(const Duration(milliseconds: 1000))
352
+ : Debounce(const Duration(milliseconds: 500));
353
354
_depositAmountDebounce.run(() {
355
widget.exchangeViewModel.calculateBestRate();
368
});
369
370
receiveAddressController.addListener(
318
- () => widget.exchangeViewModel.receiveAddress = receiveAddressController.text);
371
+ () => widget.exchangeViewModel.receiveAddress = receiveAddressController.text,
372
+ );
373
374
receiveAmountController.addListener(() {
375
if (receiveAmountController.text != widget.exchangeViewModel.receiveAmount) {
385
}
386
});
387
334
- reaction((_) => widget.exchangeViewModel.wallet.walletAddresses.addressForExchange,
335
- (String address) {
388
+ _disposers.add(
389
+ reaction((_) => widget.exchangeViewModel.wallet.walletAddresses.addressForExchange,
390
+ (String address) {
391
if (widget.exchangeViewModel.depositCurrency == CryptoCurrency.xmr) {
392
depositKey.currentState!.changeAddress(address: address);
393
}
395
// if (widget.exchangeViewModel.receiveCurrency == CryptoCurrency.xmr) {
396
// receiveKey.currentState!.changeAddress(address: address);
397
// }
343
- });
398
+ }));
399
400
_depositAddressFocus.addListener(() async {
401
if (!_depositAddressFocus.hasFocus && depositAddressController.text.isNotEmpty) {
433
widget.exchangeViewModel.setCanonicalReceiveAmount(widget.initialPaymentRequest!.amount);
434
widget.exchangeViewModel.receiveAddress = widget.initialPaymentRequest!.address;
435
} catch (e) {
381
- printV('error: ${e.toString()}');
436
+ printV("error: ${e.toString()}");
437
// TODO
438
}
439
}
440
+
441
+ if (widget.fromSend != null) {
442
+ widget.exchangeViewModel.changeReceiveCurrency(currency: widget.fromSend!.receiveCurrency);
443
+ widget.exchangeViewModel.receiveAddress = widget.fromSend!.recipientAddress;
444
+ widget.exchangeViewModel.depositAddress =
445
+ widget.exchangeViewModel.wallet.walletAddresses.addressForExchange;
446
+ widget.exchangeViewModel.enableFixedRateMode();
447
+ final receiveAmount = widget.fromSend!.receiveAmount;
448
+ if (receiveAmount != null) {
449
+ widget.exchangeViewModel.setCanonicalReceiveAmount(receiveAmount.toString());
450
+ }
451
+ widget.exchangeViewModel.calculateBestRate();
452
+ }
453
});
454
}
455
456
@override
457
void dispose() {
458
+ for (final disposer in _disposers) {
459
+ disposer();
460
+ }
461
+ _receiveAmountDebounce.cancel();
462
+ _depositAmountDebounce.cancel();
463
_depositAmountFocus.dispose();
464
_depositAddressFocus.dispose();
465
_receiveAmountFocus.dispose();
468
super.dispose();
469
}
470
398
- void _onCurrencyChange(CryptoCurrency currency, ExchangeViewModel exchangeViewModel,
399
- GlobalKey<SwapAmountBoxState> key) {
471
+ void _onCurrencyChange(
472
+ CryptoCurrency currency,
473
+ ExchangeViewModel exchangeViewModel,
474
+ GlobalKey<SwapAmountBoxState> key,
475
+ ) {
476
final isCurrentTypeWallet = exchangeViewModel.useSameWalletAddress(currency);
477
402
- if (key == depositKey && !isCurrentTypeWallet) exchangeViewModel.isSendFromExternal = true;
403
- if (key == depositKey && isCurrentTypeWallet) exchangeViewModel.isSendFromExternal = false;
478
+ if (key == depositKey && !isCurrentTypeWallet) {
479
+ exchangeViewModel.isSendFromExternal = true;
480
+ }
481
+ if (key == depositKey && isCurrentTypeWallet) {
482
+ exchangeViewModel.isSendFromExternal = false;
483
+ }
484
485
if (key == depositKey) {
486
key.currentState!.changeAddress(
487
address:
408
- isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.addressForExchange : '',
488
+ isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.addressForExchange : "",
489
);
490
}
491
}
492
413
- void _onWalletNameChange(ExchangeViewModel exchangeViewModel, CryptoCurrency currency,
414
- GlobalKey<SwapAmountBoxState> key) {
493
+ void _onWalletNameChange(
494
+ ExchangeViewModel exchangeViewModel,
495
+ CryptoCurrency currency,
496
+ GlobalKey<SwapAmountBoxState> key,
497
+ ) {
498
final isCurrentTypeWallet = exchangeViewModel.useSameWalletAddress(currency);
499
500
if (isCurrentTypeWallet) {
502
exchangeViewModel.wallet.walletAddresses.addressForExchange;
503
} else if (key.currentState!.addressController.text ==
504
exchangeViewModel.wallet.walletAddresses.addressForExchange) {
422
- key.currentState!.addressController.text = '';
505
+ key.currentState!.addressController.text = "";
506
}
507
}
508
509
Future<String> fetchParsedAddress(
427
- BuildContext context, String domain, CryptoCurrency currency) async {
510
+ BuildContext context,
511
+ String domain,
512
+ CryptoCurrency currency,
513
+ ) async {
514
printV("$domain");
515
final parsedAddress = await widget.adrResService
516
.resolve(query: domain, wallet: widget.exchangeViewModel.wallet, currency: currency);
520
}
521
522
void _showFeeAlert(BuildContext context) async {
437
- await Future<void>.delayed(Duration(seconds: 1));
523
+ await Future<void>.delayed(const Duration(seconds: 1));
524
if (!context.mounted) {
525
return;
526
}
527
528
final confirmed = await showPopUp<bool>(
443
- context: context,
444
- builder: (dialogContext) {
445
- return AlertWithTwoActions(
446
- alertTitle: S.of(context).low_fee,
447
- alertContent: S.of(context).low_fee_alert,
448
- leftButtonText: S.of(context).ignor,
449
- rightButtonText: S.of(context).use_suggested,
450
- actionLeftButton: () => Navigator.of(dialogContext).pop(false),
451
- actionRightButton: () => Navigator.of(dialogContext).pop(true));
452
- }) ??
529
+ context: context,
530
+ builder: (dialogContext) => AlertWithTwoActions(
531
+ alertTitle: S.of(context).low_fee,
532
+ alertContent: S.of(context).low_fee_alert,
533
+ leftButtonText: S.of(context).ignor,
534
+ rightButtonText: S.of(context).use_suggested,
535
+ actionLeftButton: () => Navigator.of(dialogContext).pop(false),
536
+ actionRightButton: () => Navigator.of(dialogContext).pop(true),
537
+ ),
538
+ ) ??
539
false;
540
if (confirmed) {
541
widget.exchangeViewModel.feesViewModel.setDefaultTransactionPriority();
542
}
543
}
544
459
- void disposeBestRateSync() => widget.exchangeViewModel.bestRateSync.cancel();
460
-
545
@override
546
Widget build(BuildContext context) {
547
+ final colorScheme = Theme.of(context).colorScheme;
548
+
549
+ final fromSend = widget.fromSend;
550
+ final fromType = widget.exchangeViewModel.wallet.type;
551
+ final fromName = walletTypeToString(fromType);
552
+ final fromIcon = symbolIconPathForWalletType(fromType) ?? "";
553
+
554
+ final targetType = fromSend?.targetWalletType;
555
+ final toName = targetType != null ? walletTypeToString(targetType) : "";
556
+ final toIcon = (targetType != null ? symbolIconPathForWalletType(targetType) : null) ?? "";
557
+
558
return KeyboardHideOverlay(
559
unfocusOnTap: true,
560
child: Container(
561
decoration: BoxDecoration(
467
- color: Theme.of(context).colorScheme.surface,
468
- borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
562
+ color: colorScheme.surface,
563
+ borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
564
),
565
child: Column(
566
children: [
567
ModalTopBar(
473
- title: S.of(context).swap,
474
- leadingIcon: Icon(Icons.close),
568
+ title:
569
+ fromSend != null ? S.of(context).swap_from_network(fromName) : S.of(context).swap,
570
+ leadingIcon: Icon(fromSend != null ? Icons.arrow_back_ios_new : Icons.close),
571
leadingSemanticLabel: S.of(context).close,
572
onLeadingPressed: Navigator.of(context).maybePop,
477
- trailingIcon: CakeImageWidget(
478
- imageUrl: "assets/new-ui/options.svg",
479
- colorFilter:
480
- ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
481
- ),
573
+ trailingIcon: fromSend != null
574
+ ? null
575
+ : CakeImageWidget(
576
+ imageUrl: "assets/new-ui/options.svg",
577
+ colorFilter: ColorFilter.mode(colorScheme.primary, BlendMode.srcIn),
578
+ ),
579
trailingSemanticLabel: S.of(context).configure,
580
onTrailingPressed: () {
484
- Navigator.of(context).push(CupertinoPageRoute(
581
+ Navigator.of(context).push(
582
+ CupertinoPageRoute(
583
builder: (context) => Material(
486
- child: SwapOptionsPage(exchangeViewModel: widget.exchangeViewModel))));
584
+ child: SwapOptionsPage(exchangeViewModel: widget.exchangeViewModel),
585
+ ),
586
+ ),
587
+ );
588
},
589
),
590
+ const SizedBox(height: 24),
591
Expanded(
592
child: SafeArea(
593
child: Padding(
492
- padding: const EdgeInsets.symmetric(horizontal: 18.0),
594
+ padding: const EdgeInsets.symmetric(horizontal: 18),
595
child: Column(
596
mainAxisAlignment: MainAxisAlignment.spaceBetween,
597
mainAxisSize: MainAxisSize.max,
605
controller: ModalScrollController.of(context),
606
child: Column(
607
children: [
608
+ if (fromSend != null)
609
+ Padding(
610
+ padding: const EdgeInsets.only(bottom: 12),
611
+ child: SwapSectionHeader(
612
+ label: S.of(context).from,
613
+ networkName: fromName,
614
+ networkIconPath: fromIcon,
615
+ ),
616
+ ),
617
Observer(
618
builder: (_) => SwapAmountBox(
619
isReceiverCard: false,
620
walletSwitcherViewModel: widget.walletSwitcherViewModel,
621
exchangeViewModel: widget.exchangeViewModel,
511
- onDispose: disposeBestRateSync,
622
hasAllAmount: widget.exchangeViewModel.hasAllAmount,
623
allAmount: widget.exchangeViewModel.hasAllAmount
624
? () => widget.exchangeViewModel.enableSendAllAmount()
625
: null,
626
key: depositKey,
517
- title: S.of(context).send,
627
+ title: fromSend != null ? "" : S.of(context).send,
628
+ sourceSelectorMode: fromSend != null,
629
+ walletName: fromSend != null
630
+ ? widget.exchangeViewModel.wallet.name
631
+ : null,
632
+ balanceByAsset: fromSend?.depositBalanceByAsset,
633
+ useSingleNetworkLayout: fromSend != null,
634
+ filteredNetwork: fromSend != null
635
+ ? widget.exchangeViewModel.wallet.type
636
+ : null,
637
currency: widget.exchangeViewModel.depositCurrency,
638
useBaseUnit: widget.exchangeViewModel.useDepositBaseUnit,
639
hasRefundAddress: true,
640
currencies: widget.exchangeViewModel.depositCurrencies,
641
onCurrencySelected: (currency) {
523
- if (currency is CryptoCurrency) {
524
- widget.exchangeViewModel
525
- .changeDepositCurrency(currency: currency);
642
+ if (currency is! CryptoCurrency) {
643
+ return;
644
+ }
645
+ final vm = widget.exchangeViewModel;
646
+ if (fromSend == null) {
647
+ vm.changeDepositCurrency(currency: currency);
648
+ return;
649
+ }
650
+ final keepReceive = vm.receiveAmount;
651
+ vm.changeDepositCurrency(currency: currency);
652
+ vm.enableFixedRateMode();
653
+ if (keepReceive.isNotEmpty) {
654
+ vm.changeReceiveAmount(amount: keepReceive);
655
+ } else {
656
+ vm.calculateBestRate();
657
}
658
},
528
- currencyValueValidator: (value) {
529
- return !widget.exchangeViewModel.isFixedRateMode &&
530
- value != S.of(context).all
531
- ? AmountValidator(
532
- isAutovalidate: true,
533
- currency: widget.exchangeViewModel.isFixedRateMode
534
- ? widget.exchangeViewModel.receiveCurrency
535
- : widget.exchangeViewModel.depositCurrency,
536
- minValue:
537
- widget.exchangeViewModel.limits.min.toString(),
538
- maxValue:
539
- widget.exchangeViewModel.limits.max.toString(),
540
- amountParsingProxy:
541
- widget.exchangeViewModel.amountParsingProxy,
542
- ).call(value)
543
- : null;
544
- },
659
+ currencyValueValidator: (value) => !widget
660
+ .exchangeViewModel.isFixedRateMode &&
661
+ value != S.of(context).all
662
+ ? AmountValidator(
663
+ isAutovalidate: true,
664
+ currency: widget.exchangeViewModel.isFixedRateMode
665
+ ? widget.exchangeViewModel.receiveCurrency
666
+ : widget.exchangeViewModel.depositCurrency,
667
+ minValue:
668
+ widget.exchangeViewModel.limits.min.toString(),
669
+ maxValue:
670
+ widget.exchangeViewModel.limits.max.toString(),
671
+ amountParsingProxy:
672
+ widget.exchangeViewModel.amountParsingProxy,
673
+ ).call(value)
674
+ : null,
675
addressTextFieldValidator: AddressValidator(
546
- type: widget.exchangeViewModel.depositCurrency),
676
+ type: widget.exchangeViewModel.depositCurrency,
677
+ ),
678
onPushPasteButton: (context) async {
548
- final clipboard = await Clipboard.getData('text/plain');
679
+ final clipboard = await Clipboard.getData("text/plain");
680
widget.exchangeViewModel.depositAddress =
550
- clipboard?.text ?? '';
681
+ clipboard?.text ?? "";
682
683
final domain = widget.exchangeViewModel.depositAddress;
684
widget.exchangeViewModel.depositAddress =
554
- await fetchParsedAddress(context, domain,
555
- widget.exchangeViewModel.depositCurrency);
685
+ await fetchParsedAddress(
686
+ context,
687
+ domain,
688
+ widget.exchangeViewModel.depositCurrency,
689
+ );
690
},
691
onPushAddressBookButton: (context) async {
692
final domain = widget.exchangeViewModel.depositAddress;
693
widget.exchangeViewModel.depositAddress =
560
- await fetchParsedAddress(context, domain,
561
- widget.exchangeViewModel.depositCurrency);
694
+ await fetchParsedAddress(
695
+ context,
696
+ domain,
697
+ widget.exchangeViewModel.depositCurrency,
698
+ );
699
},
700
),
701
),
702
SwapLimitPopup(exchangeViewModel: widget.exchangeViewModel),
566
- Padding(
567
- padding: const EdgeInsets.symmetric(vertical: 12.0),
568
- child: Stack(
569
- alignment: Alignment.center,
703
+ if (fromSend != null)
704
+ Column(
705
children: [
571
- Container(
572
- height: 1,
573
- width: double.infinity,
574
- color: Theme.of(context).colorScheme.surfaceContainerHigh,
575
- ),
576
- ModernButton.svg(
577
- size: 36,
578
- iconSize: 24,
579
- svgPath: "assets/new-ui/swap_amounts.svg",
580
- onPressed: widget.exchangeViewModel.reverseSwapDirection,
706
+ const SizedBox(height: 24),
707
+ Icon(
708
+ Icons.arrow_downward,
709
+ size: 24,
710
+ color: colorScheme.onSurfaceVariant,
711
semanticLabel: S.of(context).swap_reverse_direction,
712
),
713
+ const SizedBox(height: 24),
714
],
715
+ )
716
+ else
717
+ Padding(
718
+ padding: const EdgeInsets.symmetric(vertical: 12),
719
+ child: Stack(
720
+ alignment: Alignment.center,
721
+ children: [
722
+ Container(
723
+ height: 1,
724
+ width: double.infinity,
725
+ color: colorScheme.surfaceContainerHigh,
726
+ ),
727
+ ModernButton.svg(
728
+ size: 36,
729
+ iconSize: 24,
730
+ svgPath: "assets/new-ui/swap_amounts.svg",
731
+ onPressed:
732
+ widget.exchangeViewModel.reverseSwapDirection,
733
+ ),
734
+ ],
735
+ ),
736
+ ),
737
+ if (fromSend != null)
738
+ Padding(
739
+ padding: const EdgeInsets.only(bottom: 12),
740
+ child: SwapSectionHeader(
741
+ label: S.of(context).to,
742
+ networkName: toName,
743
+ networkIconPath: toIcon,
744
+ ),
745
),
585
- ),
746
Observer(
747
builder: (_) => SwapAmountBox(
748
isReceiverCard: true,
749
walletSwitcherViewModel: widget.walletSwitcherViewModel,
750
exchangeViewModel: widget.exchangeViewModel,
591
- onDispose: disposeBestRateSync,
751
key: receiveKey,
593
- title: S.of(context).receive,
752
+ title: fromSend != null ? "" : S.of(context).receive,
753
+ filteredNetwork:
754
+ fromSend != null ? fromSend.targetWalletType : null,
755
currencies: widget.exchangeViewModel.receiveCurrencies,
756
currency: widget.exchangeViewModel.receiveCurrency,
757
useBaseUnit: widget.exchangeViewModel.useReceiveBaseUnit,
758
onCurrencySelected: (currency) {
598
- if (currency is CryptoCurrency) {
599
- widget.exchangeViewModel
600
- .changeReceiveCurrency(currency: currency);
759
+ if (currency is! CryptoCurrency) {
760
+ return;
761
+ }
762
+ final vm = widget.exchangeViewModel;
763
+ final keepAddress = vm.receiveAddress;
764
+ vm.changeReceiveCurrency(currency: currency);
765
+ if (fromSend != null) {
766
+ if (keepAddress.isNotEmpty) {
767
+ vm.receiveAddress = keepAddress;
768
+ }
769
+ vm.enableFixedRateMode();
770
+ vm.calculateBestRate();
771
}
772
},
603
- currencyValueValidator: (value) {
604
- return widget.exchangeViewModel.isFixedRateMode
605
- ? AmountValidator(
606
- isAutovalidate: true,
607
- currency: widget.exchangeViewModel.receiveCurrency,
608
- minValue:
609
- widget.exchangeViewModel.limits.min.toString(),
610
- maxValue:
611
- widget.exchangeViewModel.limits.max.toString(),
612
- amountParsingProxy:
613
- widget.exchangeViewModel.amountParsingProxy,
614
- ).call(value)
615
- : null;
616
- },
773
+ currencyValueValidator: (value) => widget
774
+ .exchangeViewModel.isFixedRateMode
775
+ ? AmountValidator(
776
+ isAutovalidate: true,
777
+ currency: widget.exchangeViewModel.receiveCurrency,
778
+ minValue:
779
+ widget.exchangeViewModel.limits.min.toString(),
780
+ maxValue:
781
+ widget.exchangeViewModel.limits.max.toString(),
782
+ amountParsingProxy:
783
+ widget.exchangeViewModel.amountParsingProxy,
784
+ ).call(value)
785
+ : null,
786
addressTextFieldValidator: AddressValidator(
618
- type: widget.exchangeViewModel.receiveCurrency),
787
+ type: widget.exchangeViewModel.receiveCurrency,
788
+ ),
789
onPushPasteButton: (context) async {
620
- final clipboard = await Clipboard.getData('text/plain');
790
+ final clipboard = await Clipboard.getData("text/plain");
791
widget.exchangeViewModel.receiveAddress =
622
- clipboard?.text ?? '';
792
+ clipboard?.text ?? "";
793
794
final domain = widget.exchangeViewModel.receiveAddress;
795
widget.exchangeViewModel.receiveAddress =
626
- await fetchParsedAddress(context, domain,
627
- widget.exchangeViewModel.receiveCurrency);
796
+ await fetchParsedAddress(
797
+ context,
798
+ domain,
799
+ widget.exchangeViewModel.receiveCurrency,
800
+ );
801
},
802
onPushAddressBookButton: (context) async {
803
final domain = widget.exchangeViewModel.receiveAddress;
804
widget.exchangeViewModel.receiveAddress =
632
- await fetchParsedAddress(context, domain,
633
- widget.exchangeViewModel.receiveCurrency);
805
+ await fetchParsedAddress(
806
+ context,
807
+ domain,
808
+ widget.exchangeViewModel.receiveCurrency,
809
+ );
810
},
811
),
636
- )
812
+ ),
813
],
814
),
815
),
823
children: [
824
if (widget.exchangeViewModel.status is! SyncedSyncStatus)
825
SendSyncingIndicator(status: widget.exchangeViewModel.status),
650
- if (widget.exchangeViewModel.isFixedRateMode)
651
- Text(S.of(context).exchange_rate_is_fixed,
652
- style: TextStyle(
653
- color: Theme.of(context).colorScheme.onSurfaceVariant,
654
- fontSize: 12)),
655
- SwapProviderPreview(exchangeViewModel: widget.exchangeViewModel),
826
+ if (fromSend == null && widget.exchangeViewModel.isFixedRateMode)
827
+ Text(
828
+ S.of(context).exchange_rate_is_fixed,
829
+ style: TextStyle(color: colorScheme.onSurfaceVariant, fontSize: 12),
830
+ ),
831
+ if (fromSend != null)
832
+ AnyPaySwapFooter(exchangeViewModel: widget.exchangeViewModel)
833
+ else
834
+ SwapProviderPreview(exchangeViewModel: widget.exchangeViewModel),
835
Observer(
836
builder: (_) => LoadingPrimaryButton(
658
- key: ValueKey('exchange_page_exchange_button_key'),
837
+ key: const ValueKey("exchange_page_exchange_button_key"),
838
text: widget.exchangeViewModel.isAvailableInSelected
839
? S.of(context).swap
840
: S.of(context).change_selected_exchanges,
846
if (_shouldWaitTillSynced) {
847
showPopUp<void>(
848
context: context,
670
- builder: (BuildContext context) {
671
- return AlertWithOneAction(
672
- alertTitle: S.of(context).exchange,
673
- alertContent:
674
- S.of(context).exchange_sync_alert_content,
675
- buttonText: S.of(context).ok,
676
- buttonAction: () => Navigator.of(context).pop(),
677
- );
678
- },
849
+ builder: (BuildContext context) => AlertWithOneAction(
850
+ alertTitle: S.of(context).exchange,
851
+ alertContent:
852
+ S.of(context).exchange_sync_alert_content,
853
+ buttonText: S.of(context).ok,
854
+ buttonAction: () => Navigator.of(context).pop(),
855
+ ),
856
);
857
} else {
858
final check =
870
}
871
}
872
: () => PresentProviderPicker(
696
- exchangeViewModel: widget.exchangeViewModel)
697
- .presentProviderPicker(context),
698
- color: Theme.of(context).colorScheme.primary,
699
- textColor: Theme.of(context).colorScheme.onPrimary,
873
+ exchangeViewModel: widget.exchangeViewModel,
874
+ ).presentProviderPicker(context),
875
+ color: colorScheme.primary,
876
+ textColor: colorScheme.onPrimary,
877
isDisabled: _swapButtonDisabled(),
878
isLoading: widget.exchangeViewModel.tradeState is TradeIsCreating,
879
),
880
),
881
],
882
),
706
- )
883
+ ),
884
],
885
),
886
),
887
),
711
- )
888
+ ),
889
],
890
),
891
),
893
}
894
895
bool _swapButtonDisabled() {
719
- if (widget.exchangeViewModel.selectedProviders.isEmpty) return true;
896
+ if (widget.exchangeViewModel.selectedProviders.isEmpty) {
897
+ return true;
898
+ }
899
721
- if (widget.exchangeViewModel.receiveAddress.isEmpty) return true;
900
+ if (widget.exchangeViewModel.receiveAddress.isEmpty) {
901
+ return true;
902
+ }
903
723
- if (widget.exchangeViewModel.status is! SyncedSyncStatus) return true;
904
+ if (widget.exchangeViewModel.status is! SyncedSyncStatus) {
905
+ return true;
906
+ }
907
725
- if (widget.exchangeViewModel.depositAmount.isEmpty) return true;
908
+ if (widget.exchangeViewModel.depositAmount.isEmpty) {
909
+ return true;
910
+ }
911
912
return false;
913
}
914
}
915
916
class SwapProviderPreview extends StatelessWidget {
732
- const SwapProviderPreview({super.key, required this.exchangeViewModel});
917
+ const SwapProviderPreview({
918
+ required this.exchangeViewModel,
919
+ super.key,
920
+ });
921
922
final ExchangeViewModel exchangeViewModel;
923
924
@override
737
- Widget build(BuildContext context) {
738
- return Observer(builder: (_) {
739
- if (exchangeViewModel.depositAmount.isEmpty) return SizedBox.shrink();
740
-
741
- final provider = exchangeViewModel.forcedProvider ?? exchangeViewModel.providerDisplay;
742
- final rate = exchangeViewModel.forcedProvider == null
743
- ? exchangeViewModel.bestRate
744
- : exchangeViewModel.forcedProviderRate;
745
-
746
- return GestureDetector(
747
- onTap: () {
748
- if (provider != null) {
749
- Navigator.of(context).push(CupertinoPageRoute(
750
- builder: (context) => Material(
751
- child: ProviderSelectorPage(exchangeViewModel: exchangeViewModel),
752
- ),
753
- ));
925
+ Widget build(BuildContext context) => Observer(
926
+ builder: (_) {
927
+ final isFetchingDeposit =
928
+ exchangeViewModel.isFixedRateMode && exchangeViewModel.receiveAmount.isNotEmpty;
929
+ if (exchangeViewModel.depositAmount.isEmpty && !isFetchingDeposit) {
930
+ return const SizedBox.shrink();
931
}
755
- },
756
- child: Container(
757
- height: 48,
758
- decoration: BoxDecoration(
759
- color: Theme.of(context).colorScheme.surfaceContainer,
760
- borderRadius: BorderRadius.circular(16),
761
- ),
762
- child: Padding(
763
- padding: const EdgeInsets.symmetric(horizontal: 12.0),
764
- child: Row(
765
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
766
- children: [
767
- Row(
768
- spacing: 12,
769
- children: [
770
- if (provider != null)
771
- CakeImageWidget(imageUrl: provider.description.image, width: 28, height: 28),
772
- if (provider == null) CupertinoActivityIndicator(),
773
- Text(
774
- provider?.title ?? "${S.of(context).finding_provider}...",
775
- style: TextStyle(
776
- fontSize: 14,
777
- fontWeight: FontWeight.w400,
778
- color: provider == null
779
- ? Theme.of(context).colorScheme.onSurfaceVariant
780
- : Theme.of(context).colorScheme.onSurface),
781
- )
782
- ],
783
- ),
784
- if (provider != null)
785
- Row(
786
- children: [
787
- Text(
788
- "1 ${exchangeViewModel.depositCurrency} ≈ ${rate.toStringAsFixed(6)} ${exchangeViewModel.receiveCurrency}",
789
- style: TextStyle(
790
- fontSize: 14,
791
- fontWeight: FontWeight.w400,
792
- color: Theme.of(context).colorScheme.onSurfaceVariant),
793
- ),
794
- CakeImageWidget(
795
- imageUrl: "assets/new-ui/chooser.svg",
796
- colorFilter: ColorFilter.mode(
797
- Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
798
- )
799
- ],
800
- )
801
- ],
802
- ),
803
- ),
804
- ),
805
- );
806
- });
807
- }
808
-}
809
-
810
-class SwapAmountBox extends StatefulWidget {
811
- SwapAmountBox({
812
- Key? key,
813
- required this.currency,
814
- required this.currencies,
815
- required this.onCurrencySelected,
816
- this.currencyValueValidator,
817
- this.addressTextFieldValidator,
818
- this.title = '',
819
- this.hasRefundAddress = false,
820
- this.hasAllAmount = false,
821
- this.useBaseUnit = false,
822
- this.allAmount,
823
- this.onPushPasteButton,
824
- this.onPushAddressBookButton,
825
- this.onDispose,
826
- required this.exchangeViewModel,
827
- required this.isReceiverCard,
828
- required this.walletSwitcherViewModel,
829
- }) : super(key: key);
830
-
831
- final List<Currency> currencies;
832
- final Function(Currency) onCurrencySelected;
833
- final String title;
834
- final bool isReceiverCard;
835
- final Currency currency;
836
- final bool useBaseUnit;
837
- final bool hasRefundAddress;
838
- final FormFieldValidator<String>? currencyValueValidator;
839
- final FormFieldValidator<String>? addressTextFieldValidator;
840
- final FormFieldValidator<String> allAmountValidator = AllAmountValidator();
841
- final bool hasAllAmount;
842
- final VoidCallback? allAmount;
843
- final void Function(BuildContext context)? onPushPasteButton;
844
- final void Function(BuildContext context)? onPushAddressBookButton;
845
- final Function()? onDispose;
846
- final ExchangeViewModel exchangeViewModel;
847
- final WalletSwitcherViewModel walletSwitcherViewModel;
848
-
849
- @override
850
- State<SwapAmountBox> createState() => SwapAmountBoxState();
851
-}
852
-
853
-class SwapAmountBoxState extends State<SwapAmountBox> {
854
- final addressController = TextEditingController();
855
- final amountController = TextEditingController();
856
- final fiatAmountController = TextEditingController();
857
- final amountFocusNode = FocusNode();
858
- final memoController = TextEditingController();
859
- ReactionDisposer? _memoReactionDisposer;
860
- VoidCallback? _memoListener;
861
-
862
- @override
863
- void initState() {
864
- if (widget.isReceiverCard) {
865
- memoController.text = widget.exchangeViewModel.receiveAddressExtraId;
932
867
- _memoListener = () {
868
- if (widget.exchangeViewModel.receiveAddressExtraId != memoController.text) {
869
- widget.exchangeViewModel.receiveAddressExtraId = memoController.text;
870
- }
871
- };
872
- memoController.addListener(_memoListener!);
873
-
874
- _memoReactionDisposer =
875
- reaction((_) => widget.exchangeViewModel.receiveAddressExtraId, (String value) {
876
- if (memoController.text != value) {
877
- memoController.text = value;
878
- }
879
- });
880
- }
881
-
882
- super.initState();
883
- }
884
-
885
- @override
886
- void dispose() {
887
- if (_memoListener != null) {
888
- memoController.removeListener(_memoListener!);
889
- }
890
- _memoReactionDisposer?.call();
891
- memoController.dispose();
892
- super.dispose();
893
- }
894
-
895
- bool _fiatInputMode = false;
933
+ final provider = exchangeViewModel.forcedProvider ?? exchangeViewModel.providerDisplay;
934
+ final rate = exchangeViewModel.forcedProvider == null
935
+ ? exchangeViewModel.bestRate
936
+ : exchangeViewModel.forcedProviderRate;
937
897
- @override
898
- Widget build(BuildContext context) {
899
- final currencyToShow = (widget.currency is CryptoCurrency)
900
- ? widget.exchangeViewModel.amountParsingProxy
901
- .getCryptoSymbol(widget.currency as CryptoCurrency)
902
- : widget.currency.name.toUpperCase();
903
-
904
- final chainIconPath = (widget.currency is CryptoCurrency)
905
- ? _getCurrencyChainIconPath(widget.currency as CryptoCurrency)
906
- : null;
907
-
908
- return Column(
909
- spacing: 12,
910
- crossAxisAlignment: CrossAxisAlignment.start,
911
- children: [
912
- Text(
913
- widget.title,
914
- style: TextStyle(
915
- fontSize: 12,
916
- fontWeight: FontWeight.w500,
917
- color: Theme.of(context).colorScheme.onSurfaceVariant),
918
- ),
919
- Container(
920
- decoration: ShapeDecoration(
921
- color: Theme.of(context).colorScheme.surfaceContainer,
922
- shape: RoundedSuperellipseBorder(borderRadius: BorderRadius.circular(24))),
923
- child: Padding(
924
- padding: const EdgeInsets.all(16.0),
925
- child: Column(
926
- spacing: 12,
927
- children: [
928
- GestureDetector(
929
- behavior: HitTestBehavior.opaque,
930
- onTap: amountFocusNode.requestFocus,
931
- child: Row(
932
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
933
- spacing: 4,
934
- children: [
935
- Expanded(
936
- child: Row(
937
- spacing: 4,
938
- crossAxisAlignment: CrossAxisAlignment.center,
939
- children: [
940
- Flexible(
941
- child: IntrinsicWidth(
942
- child: TextFormField(
943
- keyboardType: TextInputType.numberWithOptions(
944
- signed: false,
945
- decimal: !widget.useBaseUnit,
946
- ),
947
- validator: _fiatInputMode ? null : widget.currencyValueValidator,
948
- controller:
949
- _fiatInputMode ? fiatAmountController : amountController,
950
- focusNode: amountFocusNode,
951
- style: TextStyle(
952
- fontSize: 28,
953
- color: Theme.of(context).colorScheme.onSurface,
954
- fontWeight: FontWeight.w400,
955
- ),
956
- decoration: InputDecoration(
957
- contentPadding: EdgeInsets.zero,
958
- isDense: true,
959
- hintText: "0",
960
- fillColor: Colors.transparent,
961
- hoverColor: Colors.transparent,
962
- focusedBorder: InputBorder.none,
963
- enabledBorder: InputBorder.none,
964
- ),
965
- inputFormatters: <TextInputFormatter>[
966
- DecimalInputFormatter(
967
- maxDecimals:
968
- widget.useBaseUnit ? 0 : widget.currency.decimals,
969
- ),
970
- ],
971
- ),
972
- ),
973
- ),
974
- if (_fiatInputMode)
975
- Center(
976
- child: Text(
977
- widget.exchangeViewModel.fiat.title,
978
- style: TextStyle(
979
- fontSize: 16,
980
- color: Theme.of(context).colorScheme.onSurfaceVariant,
981
- ),
982
- ),
983
- ),
984
- ],
985
- ),
986
- ),
987
- GestureDetector(
988
- onTap: _presentCurrencyPicker,
989
- child: Container(
990
- decoration: BoxDecoration(
991
- color: Theme.of(context).colorScheme.surface,
992
- borderRadius: BorderRadius.circular(999999),
993
- ),
994
- child: Padding(
995
- padding: const EdgeInsets.only(top: 4.0, bottom: 4, left: 4, right: 4),
996
- child: Row(
997
- mainAxisAlignment: MainAxisAlignment.center,
998
- children: [
999
- CakeImageWidget(
1000
- imageUrl: widget.currency.iconPath ?? "",
1001
- width: 28,
1002
- height: 28,
1003
- ),
1004
- const SizedBox(width: 10),
1005
- Text(
1006
- currencyToShow,
1007
- textAlign: TextAlign.center,
1008
- ),
1009
- if (chainIconPath != null && chainIconPath.isNotEmpty) ...[
1010
- SizedBox(width: 4),
1011
- CakeImageWidget(
1012
- imageUrl: chainIconPath,
1013
- width: 12,
1014
- height: 12,
1015
- colorFilter: ColorFilter.mode(
1016
- Theme.of(context).colorScheme.onSurfaceVariant,
1017
- BlendMode.srcIn,
1018
- ),
1019
- ),
1020
- const SizedBox(width: 6)
1021
- ] else
1022
- const SizedBox(width: 10),
1023
- Container(
1024
- width: 16,
1025
- height: 16,
1026
- decoration: BoxDecoration(
1027
- borderRadius: BorderRadius.circular(9999999999),
1028
- color: Theme.of(context).colorScheme.surfaceContainerHigh),
1029
- child: Padding(
1030
- padding: const EdgeInsets.all(4.0),
1031
- child: RotatedBox(
1032
- quarterTurns: 2,
1033
- child: CakeImageWidget(
1034
- imageUrl: "assets/new-ui/dropdown_arrow.svg",
1035
- width: 4,
1036
- height: 4,
1037
- colorFilter: ColorFilter.mode(
1038
- Theme.of(context).colorScheme.primary,
1039
- BlendMode.srcIn,
1040
- ),
1041
- ),
1042
- ),
1043
- ),
1044
- ),
1045
- const SizedBox(width: 4),
1046
- ],
1047
- ),
1048
- ),
1049
- ),
1050
- )
1051
- ],
938
+ return GestureDetector(
939
+ onTap: () {
940
+ if (provider != null) {
941
+ Navigator.of(context).push(
942
+ CupertinoPageRoute(
943
+ builder: (context) => Material(
944
+ child: ProviderSelectorPage(exchangeViewModel: exchangeViewModel),
945
+ ),
946
),
1053
- ),
1054
- Observer(
1055
- builder: (_) {
1056
- final hasAllAmount = !(widget.isReceiverCard ||
1057
- widget.exchangeViewModel.isSendFromExternal ||
1058
- !widget.exchangeViewModel.hasAllAmount);
1059
-
1060
- return FiatAmountBar(
1061
- foregroundElementColor: Theme.of(context).colorScheme.surfaceContainerHigh,
1062
- textColor: Theme.of(context).colorScheme.onSurfaceVariant,
1063
- allAmountColor: hasAllAmount ? null : Colors.transparent,
1064
- allAmountTextColor:
1065
- hasAllAmount ? null : Theme.of(context).colorScheme.onSurfaceVariant,
1066
- fiatInputMode: _fiatInputMode,
1067
- allAmount: !hasAllAmount
1068
- ? widget.isReceiverCard
1069
- ? null
1070
- : widget.exchangeViewModel.balanceDisplay
1071
- : widget.exchangeViewModel.depositAvailableAmount,
1072
- onSwitchButtonPressed: () {
1073
- setState(() => _fiatInputMode = !_fiatInputMode);
1074
- if (_fiatInputMode) {
1075
- updateFiatAmount();
1076
- } else {
1077
- amountController.text = widget.exchangeViewModel.depositAmount;
1078
- }
1079
- },
1080
- onAllButtonPressed: () {
1081
- setState(() {
1082
- _fiatInputMode = false;
1083
- });
1084
- widget.allAmount?.call();
1085
- },
1086
- cryptoAmount: widget.isReceiverCard
1087
- ? widget.exchangeViewModel.roundedReceiveAmount(6)
1088
- : widget.exchangeViewModel.roundedDepositAmount(6),
1089
- fiatAmount: widget.isReceiverCard
1090
- ? widget.exchangeViewModel.roundedReceiveAmountFiat(6)
1091
- : widget.exchangeViewModel.roundedDepositAmountFiat(6),
1092
- cryptoCurrencySymbol: currencyToShow,
1093
- fiatCurrencySymbol: widget.exchangeViewModel.fiat.symbol,
1094
- );
1095
- },
1096
- ),
1097
- Observer(
1098
- builder: (_) {
1099
- final addressEmpty = (widget.isReceiverCard &&
1100
- widget.exchangeViewModel.receiveAddress.isEmpty) ||
1101
- (!widget.isReceiverCard && widget.exchangeViewModel.depositAddress.isEmpty);
1102
- final addressPickerText = widget.isReceiverCard
1103
- ? (addressEmpty ? S.of(context).select_receiver : S.of(context).to)
1104
- : S.of(context).from;
1105
- final addressDescription = widget.isReceiverCard
1106
- ? widget.exchangeViewModel.receiveAddressDisplayName ??
1107
- _middleTruncate(widget.exchangeViewModel.receiveAddress, 8, 8)
1108
- : widget.exchangeViewModel.isSendFromExternal
1109
- ? S.of(context).external
1110
- : widget.exchangeViewModel.wallet.name;
1111
- return Row(
1112
- spacing: 8,
947
+ );
948
+ }
949
+ },
950
+ child: Container(
951
+ height: 48,
952
+ decoration: BoxDecoration(
953
+ color: Theme.of(context).colorScheme.surfaceContainer,
954
+ borderRadius: BorderRadius.circular(16),
955
+ ),
956
+ child: Padding(
957
+ padding: const EdgeInsets.symmetric(horizontal: 12),
958
+ child: Row(
959
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
960
+ children: [
961
+ Row(
962
+ spacing: 12,
963
children: [
1114
- Flexible(
1115
- child: GestureDetector(
1116
- onTap: _presentWalletPicker,
1117
- child: Container(
1118
- height: 36,
1119
- decoration: BoxDecoration(
1120
- color: (addressEmpty && widget.isReceiverCard)
1121
- ? Theme.of(context).colorScheme.primary
1122
- : Theme.of(context).colorScheme.surfaceContainerHigh,
1123
- borderRadius: BorderRadius.circular(9999)),
1124
- child: Padding(
1125
- padding: const EdgeInsets.symmetric(horizontal: 12.0),
1126
- child: Observer(
1127
- builder: (_) => Row(
1128
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
1129
- children: [
1130
- Row(
1131
- spacing: 8,
1132
- mainAxisSize: MainAxisSize.max,
1133
- children: [
1134
- Text(
1135
- addressPickerText,
1136
- style: TextStyle(
1137
- fontSize: 12,
1138
- fontWeight: FontWeight.w500,
1139
- color: (addressEmpty && widget.isReceiverCard)
1140
- ? Theme.of(context).colorScheme.onPrimary
1141
- : Theme.of(context).colorScheme.onSurface),
1142
- ),
1143
- Text(
1144
- addressDescription,
1145
- style: TextStyle(
1146
- fontSize: 12,
1147
- fontWeight: FontWeight.w500,
1148
- color: Theme.of(context).colorScheme.primary),
1149
- )
1150
- ],
1151
- ),
1152
- RotatedBox(
1153
- quarterTurns: 2,
1154
- child: CakeImageWidget(
1155
- imageUrl: "assets/new-ui/dropdown_arrow.svg",
1156
- colorFilter: ColorFilter.mode(
1157
- (addressEmpty && widget.isReceiverCard)
1158
- ? Theme.of(context).colorScheme.onPrimary
1159
- : Theme.of(context).colorScheme.onSurface,
1160
- BlendMode.srcIn,
1161
- ),
1162
- ),
1163
- )
1164
- ],
1165
- ),
1166
- ),
1167
- ),
1168
- ),
964
+ if (provider != null)
965
+ CakeImageWidget(
966
+ imageUrl: provider.description.image,
967
+ width: 28,
968
+ height: 28,
969
),
1170
- ),
1171
- if (!widget.isReceiverCard &&
1172
- widget.exchangeViewModel.isSendFromExternal &&
1173
- widget.exchangeViewModel.depositAddress.isEmpty)
1174
- ModernButton.svg(
1175
- svgPath: "assets/new-ui/refund_address.svg",
1176
- onPressed: askForRefundAddress,
1177
- size: 36,
1178
- iconSize: 18,
1179
- backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
1180
- semanticLabel: S.of(context).refund_address,
970
+ if (provider == null) const CupertinoActivityIndicator(),
971
+ Text(
972
+ provider?.title ?? "${S.of(context).finding_provider}...",
973
+ style: TextStyle(
974
+ fontSize: 14,
975
+ fontWeight: FontWeight.w400,
976
+ color: provider == null
977
+ ? Theme.of(context).colorScheme.onSurfaceVariant
978
+ : Theme.of(context).colorScheme.onSurface,
979
),
1182
- if (widget.isReceiverCard &&
1183
- widget.exchangeViewModel.receiveAddress.isEmpty) ...[
1184
- ModernButton.svg(
1185
- svgPath: "assets/new-ui/paste.svg",
1186
- onPressed: () => widget.onPushPasteButton?.call(context),
1187
- size: 36,
1188
- iconSize: 20,
1189
- backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
1190
- semanticLabel: S.of(context).paste,
980
+ ),
981
+ ],
982
+ ),
983
+ if (provider != null)
984
+ Row(
985
+ children: [
986
+ Text(
987
+ "1 ${exchangeViewModel.depositCurrency} ≈ ${rate.toStringAsFixed(6)} ${exchangeViewModel.receiveCurrency}",
988
+ style: TextStyle(
989
+ fontSize: 14,
990
+ fontWeight: FontWeight.w400,
991
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
992
+ ),
993
),
1192
- ModernButton.svg(
1193
- svgPath: "assets/new-ui/scan.svg",
1194
- onPressed: () => _presentQRScanner(context),
1195
- size: 36,
1196
- iconSize: 20,
1197
- backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
1198
- semanticLabel: S.of(context).scan,
994
+ CakeImageWidget(
995
+ imageUrl: "assets/new-ui/chooser.svg",
996
+ colorFilter: ColorFilter.mode(
997
+ Theme.of(context).colorScheme.onSurfaceVariant,
998
+ BlendMode.srcIn,
999
+ ),
1000
),
1200
- ]
1201
- ],
1202
- );
1203
- },
1001
+ ],
1002
+ ),
1003
+ ],
1004
),
1205
- if (widget.isReceiverCard)
1206
- Observer(builder: (_) {
1207
- final selected = widget.exchangeViewModel.receiveCurrency;
1208
- final labelType = memoLabelTypeFor(selected);
1209
- if (labelType == null) return const SizedBox.shrink();
1210
-
1211
- final isDestinationTag = labelType == MemoLabelType.destinationTag;
1212
- final hint = isDestinationTag
1213
- ? S.of(context).destination_tag_optional
1214
- : S.of(context).memo_optional;
1215
- final disclaimer = isDestinationTag
1216
- ? S.of(context).destination_tag_swap_disclaimer
1217
- : S.of(context).memo_swap_disclaimer;
1218
-
1219
- return NewSendMemoInput(
1220
- memoController: memoController,
1221
- maxMemoLength: isDestinationTag ? 20 : 256,
1222
- memoLength: memoController.text.length,
1223
- hintText: hint,
1224
- disclaimerText: disclaimer,
1225
- );
1226
- }),
1227
- ],
1228
- ),
1229
- ),
1230
- ),
1231
- ],
1232
- );
1233
- }
1234
-
1235
- void changeAddress({required String address}) {
1236
- setState(() => addressController.text = _normalizeAddressFormat(address));
1237
- }
1238
-
1239
- void changeAmount({required String amount}) {
1240
- setState(() => amountController.text = amount);
1241
- }
1242
-
1243
- String _normalizeAddressFormat(String address) {
1244
- if (address.startsWith('bitcoincash:')) address = address.substring(12);
1245
- return address;
1246
- }
1247
-
1248
- void _presentCurrencyPicker() {
1249
- final rawCurrencies = widget.isReceiverCard
1250
- ? widget.exchangeViewModel.receiveCurrencies
1251
- : widget.exchangeViewModel.depositCurrencies;
1252
- final currencies = rawCurrencies.whereType<CryptoCurrency>().toList();
1253
- appendEvmDefaultTokens(currencies);
1254
- if (widget.exchangeViewModel.wallet.type == WalletType.bitcoin) {
1255
- currencies.sort((a, b) {
1256
- if (a == CryptoCurrency.btcln) return -1;
1257
- if (b == CryptoCurrency.btcln) return 1;
1258
- return 0;
1259
- });
1260
- }
1261
-
1262
- final selected = widget.isReceiverCard
1263
- ? widget.exchangeViewModel.receiveCurrency
1264
- : widget.exchangeViewModel.depositCurrency;
1265
-
1266
- CurrencyPickerSheet.show(
1267
- context: context,
1268
- args: CurrencyPickerArgs(
1269
- items: currencies,
1270
- selected: selected,
1271
- recentsSource: RecentsSource.trades,
1272
- onSelected: widget.onCurrencySelected,
1273
- symbolResolver: widget.exchangeViewModel.amountParsingProxy.getCryptoSymbol,
1274
- ),
1275
- );
1276
- }
1277
-
1278
- Future<void> _presentQRScanner(BuildContext context) async {
1279
- bool isCameraPermissionGranted =
1280
- await PermissionHandler.checkPermission(Permission.camera, context);
1281
- if (!isCameraPermissionGranted) return;
1282
- final code = await presentQRScanner(context);
1283
- if (code == null) return;
1284
- if (code.isEmpty) return;
1285
-
1286
- try {
1287
- final uri = Uri.parse(code);
1288
- widget.exchangeViewModel.receiveAddress = uri.path;
1289
- } catch (_) {}
1290
- }
1291
-
1292
- void _presentWalletPicker() async {
1293
- final res = await showMaterialModalBottomSheet(
1294
- context: context,
1295
- backgroundColor: Colors.transparent,
1296
- builder: (context) {
1297
- return ConstrainedBox(
1298
- constraints: BoxConstraints(
1299
- maxHeight: MediaQuery.of(context).size.height * 0.6,
1300
- ),
1301
- child: SwapAddressSelectionModal(
1302
- isSelectingReceiver: widget.isReceiverCard,
1303
- exchangeViewModel: widget.exchangeViewModel,
1005
+ ),
1006
),
1007
);
1306
- });
1307
- if (res != null && res is SwapAddressSelectionResult) {
1308
- if (widget.isReceiverCard) {
1309
- widget.exchangeViewModel.selectedAddressBookWallet = res.walletInfo;
1310
- widget.exchangeViewModel.receiveAddress = res.address!;
1311
- if (res.walletInfo?.name != null) {
1312
- if (res.accountName != null) {
1313
- widget.exchangeViewModel.receiveAddressDisplayName =
1314
- "${res.walletInfo!.name} → ${res.accountName}";
1315
- } else {
1316
- widget.exchangeViewModel.receiveAddressDisplayName = res.walletInfo!.name;
1317
- }
1318
- }
1319
- } else if (res.address == null || res.address!.isEmpty) {
1320
- widget.exchangeViewModel.isSendFromExternal = true;
1321
- askForRefundAddress();
1322
- } else {
1323
- widget.exchangeViewModel.isSendFromExternal = false;
1324
- switchToDepositWallet(res.walletInfo!.name);
1325
- }
1326
- }
1327
- }
1328
-
1329
- void switchToDepositWallet(String walletName) async {
1330
- final walletType = cryptoCurrencyOrTokenToWalletType(widget.exchangeViewModel.depositCurrency);
1331
- if (walletType == null) return;
1332
- final wallet = await WalletInfo.get(walletName, walletType);
1333
- if (wallet == null) return;
1334
- widget.exchangeViewModel.depositAddress = wallet.address;
1335
- addressController.text = _normalizeAddressFormat(wallet.address);
1336
- widget.walletSwitcherViewModel.selectWallet(wallet);
1337
- await widget.walletSwitcherViewModel.switchToSelectedWallet();
1338
- }
1339
-
1340
- void askForRefundAddress() async {
1341
- final refundAddress = await showModalBottomSheet(
1342
- context: context,
1343
- isScrollControlled: true,
1344
- backgroundColor: Colors.transparent,
1345
- builder: (context) => RefundAddressModal(
1346
- selectedCurrency: widget.exchangeViewModel.depositCurrency,
1347
- isFromWalletSelection: true,
1348
- ));
1349
- if (refundAddress != null && refundAddress is String) {
1350
- widget.exchangeViewModel.depositAddress = refundAddress;
1351
- } else {
1352
- widget.exchangeViewModel.depositAddress = "";
1353
- }
1354
- }
1355
-
1356
- void updateFiatAmount() {
1357
- final newText = widget.isReceiverCard
1358
- ? widget.exchangeViewModel.receiveAmountFiat
1359
- : widget.exchangeViewModel.depositAmountFiat;
1360
-
1361
- if (double.tryParse(fiatAmountController.text) != double.tryParse(newText)) {
1362
- if (newText == "0.00") {
1363
- fiatAmountController.text = "";
1364
- } else {
1365
- fiatAmountController.text =
1366
- newText.replaceAll(RegExp(r'(?<=\.\d*)0+$'), '').replaceAll(RegExp(r'\.$'), '');
1367
- }
1368
- }
1369
- }
1370
-
1371
- String _middleTruncate(String s, int head, int tail) {
1372
- if (s.length <= head + tail + 3) return s;
1373
- return s.substring(0, head) + '...' + s.substring(s.length - tail);
1374
- }
1375
-
1376
- String? _getCurrencyChainIconPath(CryptoCurrency curr) {
1377
- try {
1378
- if (curr.chainIconPath != null) return curr.chainIconPath!;
1379
-
1380
- if (curr.tag != null) {
1381
- final currencyFromTag = CryptoCurrency.fromString(curr.tag!);
1382
-
1383
- if (currencyFromTag.chainIconPath != null) return currencyFromTag.chainIconPath!;
1384
- }
1385
- } catch (_) {}
1386
- return null;
1387
- }
1008
+ },
1009
+ );
1010
}