1
-import "package:cake_wallet/core/address_resolver/address_resolver_service.dart";
1
+import "dart:async";
2
+
3
import "package:cake_wallet/core/address_resolver/parsed_address.dart";
4
import "package:cake_wallet/core/address_validator.dart";
5
+import "package:cake_wallet/core/anypay/anypay_models.dart";
6
+import "package:cake_wallet/core/anypay/anypay_parser.dart";
7
+import "package:cake_wallet/core/anypay/anypay_service.dart";
8
import "package:cake_wallet/core/auth_service.dart";
9
import "package:cake_wallet/core/execution_state.dart";
10
import "package:cake_wallet/core/open_crypto_pay/open_cryptopay_service.dart";
7
-import "package:cake_wallet/core/universal_address_detector.dart";
8
-import "package:cake_wallet/di.dart";
11
import "package:cake_wallet/entities/contact_record.dart";
12
import "package:cake_wallet/entities/priority_for_wallet_type.dart";
13
import "package:cake_wallet/evm/evm.dart";
16
import "package:cake_wallet/monero/monero.dart";
17
import "package:cake_wallet/new-ui/modal_navigator.dart";
18
import "package:cake_wallet/new-ui/pages/coin_control_page.dart";
17
-import "package:cake_wallet/new-ui/pages/swap_page.dart";
19
import "package:cake_wallet/new-ui/widgets/animated_dropdown.dart";
19
-import "package:cake_wallet/new-ui/widgets/anypay/evm_address_detected_sheet.dart";
20
+import "package:cake_wallet/new-ui/widgets/anypay/anypay_flow.dart";
21
import "package:cake_wallet/new-ui/widgets/anypay/recipient_network_row.dart";
22
import "package:cake_wallet/new-ui/widgets/anypay/select_recipient_network_sheet.dart";
22
-import "package:cake_wallet/new-ui/widgets/anypay/send_to_network_page.dart";
23
-import "package:cake_wallet/new-ui/widgets/anypay/swap_from_network_page.dart";
24
-import "package:cake_wallet/new-ui/widgets/anypay/switch_network_wallet_page.dart";
23
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart";
24
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart";
25
import "package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart";
38
import "package:cake_wallet/new-ui/widgets/send_page/send_confirm_sheet.dart";
39
import "package:cake_wallet/new-ui/widgets/send_page/send_memo_input.dart";
40
import "package:cake_wallet/new-ui/widgets/send_page/send_syncing_indicator.dart";
43
-import "package:cake_wallet/new-ui/widgets/swap_page/swap_from_send_args.dart";
41
import "package:cake_wallet/reactions/wallet_connect.dart";
42
import "package:cake_wallet/routes.dart" show Routes;
43
import "package:cake_wallet/src/screens/connect_device/connect_device_page.dart";
44
import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
48
-import "package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart";
45
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
46
import "package:cake_wallet/src/widgets/new_list_row/list_item_regular_row_widget.dart";
47
import "package:cake_wallet/src/widgets/standard_checkbox.dart";
52
-import "package:cake_wallet/store/app_store.dart";
48
import "package:cake_wallet/utils/payment_request.dart";
49
import "package:cake_wallet/utils/show_pop_up.dart";
55
-import "package:cake_wallet/utils/token_utilities.dart";
50
import "package:cake_wallet/view_model/contact_list/contact_list_view_model.dart";
57
-import "package:cake_wallet/view_model/exchange/exchange_view_model.dart";
58
-import "package:cake_wallet/view_model/payment/payment_view_model.dart";
51
+import "package:cake_wallet/view_model/link_view_model.dart";
52
import "package:cake_wallet/view_model/send/output.dart";
53
import "package:cake_wallet/view_model/send/send_view_model.dart";
54
import "package:cake_wallet/view_model/send/send_view_model_state.dart";
59
import "package:cw_core/currency_for_wallet_type.dart";
60
import "package:cw_core/erc20_token.dart";
61
import "package:cw_core/lnurl.dart";
69
-import "package:cw_core/payment_uris.dart";
62
import "package:cw_core/transaction_priority.dart";
63
import "package:cw_core/unspent_coin_type.dart";
64
import "package:cw_core/utils/print_verbose.dart";
73
-import "package:cw_core/wallet_info.dart";
65
import "package:cw_core/wallet_type.dart";
66
import "package:flutter/cupertino.dart";
67
import "package:flutter/material.dart";
160
class SendPageParams {
161
SendPageParams({
162
this.initialPaymentRequest,
163
+ this.initialRawInput,
164
SendPageModes? mode,
165
this.unspentCoinType = UnspentCoinType.any,
166
this.initialCurrency,
167
}) : mode = mode ?? SendPageModes.normal;
168
169
final PaymentRequest? initialPaymentRequest;
170
+ final String? initialRawInput;
171
final SendPageModes mode;
172
final CryptoCurrency? initialCurrency;
173
final UnspentCoinType unspentCoinType;
176
class NewSendPage extends StatefulWidget {
177
NewSendPage({
178
required this.sendViewModel,
186
- required this.paymentViewModel,
179
+ required this.anyPayService,
180
+ required this.linkViewModel,
181
required this.walletSwitcherViewModel,
182
required this.contactListViewModel,
183
required this.authService,
184
required SendPageParams params,
185
super.key,
186
}) : initialPaymentRequest = params.initialPaymentRequest,
187
+ initialRawInput = params.initialRawInput,
188
mode = params.mode {
189
if (params.initialCurrency != null) {
190
sendViewModel.selectedCryptoCurrency = params.initialCurrency!;
192
}
193
194
final SendViewModel sendViewModel;
200
- final PaymentViewModel paymentViewModel;
195
+ final AnyPayService anyPayService;
196
+ final LinkViewModel linkViewModel;
197
final WalletSwitcherViewModel walletSwitcherViewModel;
198
final ContactListViewModel contactListViewModel;
199
final AuthService authService;
200
final PaymentRequest? initialPaymentRequest;
201
+ final String? initialRawInput;
202
final SendPageModes mode;
203
204
@override
213
final _memoControllers = <TextEditingController>[];
214
final _formKey = GlobalKey<FormState>();
215
final _addressFocusNode = FocusNode();
219
- BuildContext? loadingBottomSheetContext;
220
- BuildContext? dialogContext;
216
+ final _disposers = <ReactionDisposer>[];
217
+ StreamSubscription<Uri>? _deepLinkSubscription;
218
+ late final AnyPayFlow _anyPayFlow = AnyPayFlow(
219
+ anyPayService: widget.anyPayService,
220
+ sendViewModel: widget.sendViewModel,
221
+ authService: widget.authService,
222
+ walletSwitcherViewModel: widget.walletSwitcherViewModel,
223
+ );
224
ContactRecord? newContactAddress;
225
226
bool _justHandledPasteButton = false;
230
super.initState();
231
_addInputControllers();
232
230
- reaction((_) => widget.sendViewModel.outputs[_selectedOutput].sendAll, (all) {
231
- if (all) {
232
- widget.sendViewModel.outputs[_selectedOutput].isFiatEntry = false;
233
- _amountControllers[_selectedOutput].text = S.current.all;
234
- }
235
- });
233
+ _disposers.add(
234
+ reaction((_) => widget.sendViewModel.outputs[_selectedOutput].sendAll, (all) {
235
+ if (all) {
236
+ widget.sendViewModel.outputs[_selectedOutput].isFiatEntry = false;
237
+ _amountControllers[_selectedOutput].text = S.current.all;
238
+ }
239
+ }),
240
+ );
241
237
- reaction((_) => widget.sendViewModel.outputs[_selectedOutput].address, (address) {
238
- if (_addressControllers[_selectedOutput].text != address) {
239
- _addressControllers[_selectedOutput].text = address;
240
- }
241
- });
242
+ _disposers.add(
243
+ reaction((_) => widget.sendViewModel.outputs[_selectedOutput].address, (address) {
244
+ if (_addressControllers[_selectedOutput].text != address) {
245
+ _addressControllers[_selectedOutput].text = address;
246
+ }
247
+ }),
248
+ );
249
243
- reaction((_) => widget.sendViewModel.outputs[_selectedOutput].memo, (memo) {
244
- if (memo != _memoControllers[_selectedOutput].text) {
245
- _memoControllers[_selectedOutput].text = memo;
246
- }
247
- });
250
+ _disposers.add(
251
+ reaction((_) => widget.sendViewModel.outputs[_selectedOutput].memo, (memo) {
252
+ if (memo != _memoControllers[_selectedOutput].text) {
253
+ _memoControllers[_selectedOutput].text = memo;
254
+ }
255
+ }),
256
+ );
257
249
- if (widget.initialPaymentRequest != null) {
250
- if (_isInitialRequestTypeSameAsCurrentWallet()) {
251
- final contractAddress = widget.initialPaymentRequest!.contractAddress;
252
- if (contractAddress != null && contractAddress.isNotEmpty) {
253
- WidgetsBinding.instance.addPostFrameCallback((_) async {
254
- if (!mounted) {
255
- return;
256
- }
257
- await _applyPaymentSelectingCurrency(widget.initialPaymentRequest!, null);
258
- });
259
- } else {
260
- _addressControllers[0].text = widget.initialPaymentRequest!.address;
261
- _applyNote(widget.initialPaymentRequest!.note, 0);
262
- _amountControllers[0].text = widget.initialPaymentRequest!.amount;
258
+ if (widget.initialRawInput != null || widget.initialPaymentRequest != null) {
259
+ WidgetsBinding.instance.addPostFrameCallback((_) async {
260
+ if (!mounted) {
261
+ return;
262
}
264
- } else {
265
- WidgetsBinding.instance.addPostFrameCallback(
266
- (timeStamp) {
267
- if (mounted) {
268
- final paymentRequest = widget.initialPaymentRequest!;
269
- final scheme = paymentRequest.scheme.toLowerCase();
270
- final String uri;
271
- if (scheme.isEmpty || scheme == "lightning") {
272
- uri = paymentRequest.address;
273
- } else if (scheme == "ethereum") {
274
- uri = ERC681URI(
275
- address: paymentRequest.address,
276
- amount: paymentRequest.amount,
277
- contractAddress: paymentRequest.contractAddress,
278
- chainId: paymentRequest.chainId ?? _currentEvmChainIdOrMainnet(),
279
- rawTokenAmount: paymentRequest.rawTokenAmount,
280
- ).toString();
281
- } else {
282
- final amount =
283
- paymentRequest.amount.isNotEmpty ? "?amount=${paymentRequest.amount}" : "";
284
- uri = "${paymentRequest.scheme}:${paymentRequest.address}$amount";
285
- }
286
- _handlePaymentFlow(uri, paymentRequest);
287
- }
288
- },
263
+ await _runAnyPayFlow(
264
+ rawInput: widget.initialRawInput,
265
+ paymentRequest: widget.initialPaymentRequest,
266
);
290
- }
267
+ });
268
+ }
269
+
270
+ if (widget.mode == SendPageModes.normal) {
271
+ _deepLinkSubscription = widget.linkViewModel.incomingPaymentLinks.listen((link) async {
272
+ if (!mounted) {
273
+ return;
274
+ }
275
+
276
+ final state = widget.sendViewModel.state;
277
+ if (_anyPayFlow.isSwitchingWallet ||
278
+ state is IsExecutingState ||
279
+ state is TransactionCommitting) {
280
+ printV("dropping incoming payment link while the send flow is busy");
281
+ return;
282
+ }
283
+
284
+ final route = ModalRoute.of(context);
285
+ if (route != null && !route.isCurrent) {
286
+ Navigator.of(context).popUntil((r) => r == route);
287
+ }
288
+
289
+ await _runAnyPayFlow(rawInput: link.toString());
290
+ });
291
}
292
293
_addressFocusNode.addListener(() async {
298
});
299
}
300
301
+ @override
302
+ void dispose() {
303
+ _deepLinkSubscription?.cancel();
304
+ for (final disposer in _disposers) {
305
+ disposer();
306
+ }
307
+ for (final controller in _amountControllers) {
308
+ controller.dispose();
309
+ }
310
+ for (final controller in _addressControllers) {
311
+ controller.dispose();
312
+ }
313
+ for (final controller in _memoControllers) {
314
+ controller.dispose();
315
+ }
316
+ _addressFocusNode.dispose();
317
+ super.dispose();
318
+ }
319
+
320
Future<void> _resolveAddressForOutput(Output output) async {
321
final result = await widget.sendViewModel.resolveAddressForOutput(output);
322
if (result == null || !mounted) {
345
Widget build(BuildContext context) => Observer(
346
builder: (_) {
347
final output = widget.sendViewModel.outputs[_selectedOutput];
348
+ final recipientChain = widget.sendViewModel.isEVMWallet
349
+ ? evm!.getChainInfoByChainId(_currentEvmChainIdOrMainnet())
350
+ : null;
351
return SafeArea(
352
bottom: false,
353
child: KeyboardHideOverlay(
473
output.resetParsedAddress();
474
await _resolveAddressForOutput(output);
475
454
- // Process the payment through the new flow
455
- await _handlePaymentFlow(
456
- uri.toString(),
457
- PaymentRequest.fromString(uri.toString()),
458
- );
476
+ await _runAnyPayFlow(rawInput: uri.toString());
477
},
478
onEditingComplete: _addressFocusNode.unfocus,
479
onPushAddressBookButton: (_) {
495
? output.extractedAddress
496
: output.address;
497
480
- await _handlePaymentFlow(
481
- address,
482
- PaymentRequest(
483
- address,
484
- _amountControllers[_selectedOutput].text,
485
- _memoControllers[_selectedOutput].text,
486
- "",
487
- null,
488
- ),
489
- );
498
+ await _runAnyPayPaste(address);
499
} finally {
500
_justHandledPasteButton = false;
501
}
506
widget.sendViewModel.selectedCryptoCurrency,
507
),
508
if (widget.sendViewModel.isEVMWallet &&
500
- _hasEvmRecipient(output))
509
+ _hasEvmRecipient(output) &&
510
+ recipientChain != null)
511
RecipientNetworkSelector(
502
- wallet: widget.sendViewModel.wallet,
503
- onTap: _presentRecipientNetworkPicker,
512
+ networkName: recipientChain.name,
513
+ networkIconPath: symbolIconPathForWalletType(
514
+ widget.sendViewModel.wallet.type,
515
+ ) ??
516
+ "",
517
+ onTap: () => _presentRecipientNetworkPicker(
518
+ recipientChain,
519
+ ),
520
),
521
],
522
),
1047
}
1048
1049
Future<void> _handleManualNetworkSelection(ChainInfo target) async {
1034
- final address = _addressControllers[_selectedOutput].text.trim();
1050
+ final output = widget.sendViewModel.outputs[_selectedOutput];
1051
+ final address = output.isParsedAddress
1052
+ ? output.extractedAddress
1053
+ : _addressControllers[_selectedOutput].text.trim();
1054
final note = _memoControllers[_selectedOutput].text;
1055
final isTokenSelected = widget.sendViewModel.selectedCryptoCurrency is Erc20Token;
1056
final amount = isTokenSelected ? "" : _amountControllers[_selectedOutput].text;
1038
- final paymentRequest = PaymentRequest(address, amount, note, "", null);
1039
- await _handleEvmNetworkFlow(target, paymentRequest);
1040
- }
1041
-
1042
- bool _isInitialRequestTypeSameAsCurrentWallet() {
1043
- final req = widget.initialPaymentRequest;
1044
- if (req == null) {
1045
- return false;
1046
- }
1047
-
1048
- final currentType = widget.sendViewModel.wallet.type;
1049
- if (evm != null && isEVMCompatibleChain(currentType)) {
1050
- final targetChainId = _evmTargetChainId(req);
1051
- if (targetChainId != null) {
1052
- return targetChainId == _currentEvmChainIdOrMainnet();
1053
- }
1054
- }
1055
- return widget.sendViewModel.walletCurrencyName == req.scheme.toLowerCase();
1056
- }
1057
+ final request =
1058
+ AnyPayParser.fromPaymentRequest(PaymentRequest(address, amount, note, "", null));
1059
1058
- int? _evmTargetChainId(PaymentRequest req) {
1059
- final scheme = req.scheme.toLowerCase();
1060
- if (scheme == "ethereum") {
1061
- if (req.chainId != null) {
1062
- return req.chainId;
1063
- }
1064
-
1065
- return _currentEvmChainIdOrMainnet();
1066
- }
1067
- try {
1068
- return getChainIdByCryptoCurrency(CryptoCurrency.fromString(scheme));
1069
- } catch (_) {
1070
- return null;
1060
+ final evaluation = await widget.anyPayService.evaluateForEvmChain(request, target.chainId);
1061
+ if (!mounted) {
1062
+ return;
1063
}
1064
+ await _handleEvaluation(evaluation, fallbackCurrency: target.currency);
1065
}
1066
1067
int _currentEvmChainIdOrMainnet() {
1068
final wallet = widget.sendViewModel.wallet;
1076
- if (evm != null && isEVMCompatibleChain(wallet.type)) {
1069
+ if (isEVMCompatibleChain(wallet.type)) {
1070
return evm!.getSelectedChainId(wallet) ?? evm!.getChainIdByWalletType(wallet.type);
1071
}
1072
return 1;
1073
}
1074
1082
- Future<void> _handlePaymentFlow(String uri, PaymentRequest paymentRequest) async {
1083
- final isEip681 = uri.toLowerCase().startsWith("ethereum:");
1084
- if (!isEip681 && (uri.contains("@") || paymentRequest.address.contains("@"))) {
1085
- return;
1086
- }
1087
-
1088
- if (OpenCryptoPayService.isOpenCryptoPayQR(uri) &&
1089
- widget.sendViewModel.selectedCryptoCurrency != CryptoCurrency.btcln) {
1090
- final request = await widget.sendViewModel.getOpenCryptoPayRequest(uri);
1091
- if (request == null) {
1092
- return;
1093
- }
1094
- _applyPaymentRequest(request);
1095
- return;
1096
- }
1097
-
1098
- try {
1099
- final result = await widget.paymentViewModel.processAddress(uri);
1100
-
1101
- if (paymentRequest.contractAddress != null) {
1102
- await widget.sendViewModel.fetchTokenForContractAddress(paymentRequest.contractAddress!);
1103
- }
1104
-
1105
- // This automatically switches to lightning mode if you are in a bitcoin wallet
1106
- if (result.addressDetectionResult?.detectedCurrency == CryptoCurrency.btcln) {
1107
- widget.sendViewModel.selectedCryptoCurrency = CryptoCurrency.btcln;
1108
- widget.sendViewModel.coinTypeToSpendFrom = UnspentCoinType.lightning;
1109
- }
1110
-
1111
- switch (result.type) {
1112
- case PaymentFlowType.singleWallet:
1113
- case PaymentFlowType.multipleWallets:
1114
- case PaymentFlowType.noWallets:
1115
- await _showPaymentConfirmation(
1116
- widget.paymentViewModel,
1117
- widget.walletSwitcherViewModel,
1118
- paymentRequest,
1119
- result,
1120
- );
1121
- break;
1122
- case PaymentFlowType.evmNetworkSelection:
1123
- if (result.chainId != null && evm!.getChainInfoByChainId(result.chainId!) == null) {
1124
- _showUnsupportedNetworkAlert(result.chainId!);
1125
- return;
1126
- }
1127
-
1128
- final targetChainId =
1129
- paymentRequest.scheme.isNotEmpty ? _evmTargetChainId(paymentRequest) : null;
1130
- final targetChain =
1131
- targetChainId != null ? evm!.getChainInfoByChainId(targetChainId) : null;
1132
- final currentChainId = isEVMCompatibleChain(widget.sendViewModel.wallet.type)
1133
- ? _currentEvmChainIdOrMainnet()
1134
- : null;
1135
- final isCrossChain = targetChain != null && targetChain.chainId != currentChainId;
1136
-
1137
- if (isCrossChain) {
1138
- await _handleEvmNetworkFlow(targetChain, paymentRequest);
1139
- } else if (widget.sendViewModel.isEVMWallet) {
1140
- await _applyPaymentSelectingCurrency(paymentRequest, null);
1141
- } else {
1142
- await _showEvmNetworkPicker(paymentRequest, result.walletType);
1143
- }
1144
- break;
1145
- case PaymentFlowType.solanaTokenSelection:
1146
- case PaymentFlowType.tronTokenSelection:
1147
- await _showPaymentConfirmation(
1148
- widget.paymentViewModel,
1149
- widget.walletSwitcherViewModel,
1150
- paymentRequest,
1151
- result,
1152
- );
1153
- break;
1154
- case PaymentFlowType.currentWalletCompatible:
1155
- case PaymentFlowType.error:
1156
- case PaymentFlowType.incompatible:
1157
- _applyPaymentRequest(paymentRequest);
1158
- break;
1159
- }
1160
- } catch (e) {
1161
- printV("Payment flow error: $e");
1162
- _applyPaymentRequest(paymentRequest);
1163
- }
1164
- }
1165
-
1166
- void _showUnsupportedNetworkAlert(int chainId) {
1167
- if (!mounted) {
1168
- return;
1169
- }
1170
-
1171
- showPopUp<void>(
1172
- context: context,
1173
- builder: (context) => AlertWithOneAction(
1174
- alertTitle: S.of(context).error,
1175
- alertContent: S.of(context).unsupported_network_requested(chainId.toString()),
1176
- buttonText: S.of(context).ok,
1177
- buttonAction: () => Navigator.of(context).pop(),
1178
- ),
1179
- );
1180
- }
1181
-
1182
- Future<void> _showPaymentConfirmation(
1183
- PaymentViewModel paymentViewModel,
1184
- WalletSwitcherViewModel walletSwitcherViewModel,
1185
- PaymentRequest paymentRequest,
1186
- PaymentFlowResult result,
1187
- ) async {
1188
- if (!mounted || result.walletType == null) {
1189
- return;
1190
- }
1191
-
1192
- final destinationType = result.walletType!;
1193
- final isEvmTarget = isEVMCompatibleChain(destinationType);
1194
- final destinationChainId = result.chainId;
1195
- final destinationNetworkName = networkDisplayName(destinationType, destinationChainId);
1196
- final destinationNetworkIcon = symbolIconPathForWalletType(destinationType) ?? "";
1197
-
1198
- final currentType = widget.sendViewModel.wallet.type;
1199
- final currentChainId =
1200
- isEVMCompatibleChain(currentType) && evm != null ? _currentEvmChainIdOrMainnet() : null;
1201
- final currentNetworkName = networkDisplayName(currentType, currentChainId);
1202
- final currentNetworkIcon = symbolIconPathForWalletType(currentType) ?? "";
1203
-
1204
- final hasSingleWallet =
1205
- result.type == PaymentFlowType.singleWallet || result.wallets.length == 1;
1206
- final hasMultipleWallets =
1207
- result.type == PaymentFlowType.multipleWallets || result.wallets.length > 1;
1208
- final hasWallet = hasSingleWallet || hasMultipleWallets || result.wallet != null;
1209
-
1210
- final decisionTitle = isEvmTarget
1211
- ? S.of(context).send_to_network(destinationNetworkName)
1212
- : S.of(context).network_address_detected(destinationNetworkName);
1213
- final swapConfirmTitle = isEvmTarget
1214
- ? S.of(context).swap_from_network(currentNetworkName)
1215
- : S.of(context).network_address_detected(destinationNetworkName);
1216
- final swapConfirmPrimary = isEvmTarget
1217
- ? S.of(context).continue_to_swap
1218
- : S.of(context).swap_from_network(currentNetworkName);
1219
-
1220
- if (hasWallet) {
1221
- await Navigator.of(context).push<void>(
1222
- CupertinoPageRoute(
1223
- builder: (pageContext) => SendToNetworkPage(
1224
- title: decisionTitle,
1225
- destinationNetworkName: destinationNetworkName,
1226
- destinationIconPath: destinationNetworkIcon,
1227
- currentNetworkName: currentNetworkName,
1228
- onSwitchWallet: () => _onSwitchWalletSelected(
1229
- pageContext,
1230
- result,
1231
- paymentRequest,
1232
- destinationNetworkName,
1233
- destinationNetworkIcon,
1234
- hasMultipleWallets,
1235
- ),
1236
- onSwap: () => _onSwapSelected(pageContext, result, paymentRequest),
1237
- ),
1238
- ),
1239
- );
1240
- } else {
1241
- await Navigator.of(context).push<void>(
1242
- CupertinoPageRoute(
1243
- builder: (pageContext) => SwapFromNetworkPage(
1244
- title: swapConfirmTitle,
1245
- primaryButtonText: swapConfirmPrimary,
1246
- primaryHasSwapIcon: !isEvmTarget,
1247
- destinationNetworkName: destinationNetworkName,
1248
- destinationNetworkIconPath: destinationNetworkIcon,
1249
- currentNetworkName: currentNetworkName,
1250
- currentNetworkIconPath: currentNetworkIcon,
1251
- onProceed: () => _onSwapSelected(pageContext, result, paymentRequest),
1252
- ),
1253
- ),
1254
- );
1255
- }
1256
- }
1257
-
1258
- Future<void> _onSwapSelected(
1259
- BuildContext pageContext,
1260
- PaymentFlowResult result,
1261
- PaymentRequest paymentRequest,
1262
- ) async {
1263
- await _handleSwapFlow(result, pageContext, paymentRequest);
1264
- }
1265
-
1266
- Future<void> _onSwitchWalletSelected(
1267
- BuildContext pageContext,
1268
- PaymentFlowResult result,
1269
- PaymentRequest paymentRequest,
1270
- String destName,
1271
- String destIcon,
1272
- bool hasMultipleWallets,
1273
- ) async {
1274
- WalletInfo? destinationWalletInfo;
1275
- if (hasMultipleWallets) {
1276
- destinationWalletInfo = await SwitchNetworkWalletPage.push(
1277
- context: pageContext,
1278
- networkName: destName,
1279
- targetIconPath: destIcon,
1280
- wallets: result.wallets,
1281
- );
1282
- if (destinationWalletInfo == null) {
1283
- return;
1284
- }
1285
- } else {
1286
- destinationWalletInfo =
1287
- result.wallet ?? (result.wallets.isNotEmpty ? result.wallets.first : null);
1288
- }
1289
-
1290
- if (destinationWalletInfo == null) {
1291
- return;
1292
- }
1293
-
1294
- // We may have awaited the wallet picker above, so make sure the decision page
1295
- // is still around before we reach into its navigator.
1296
- if (!pageContext.mounted) {
1297
- return;
1298
- }
1299
- if (Navigator.of(pageContext).canPop()) {
1300
- Navigator.of(pageContext).pop();
1301
- }
1302
- if (!mounted) {
1303
- return;
1304
- }
1305
- await _completeWalletSwitch(destinationWalletInfo, result, paymentRequest);
1306
- }
1307
-
1308
- Future<void> _applyPaymentSelectingCurrency(
1309
- PaymentRequest paymentRequest,
1310
- CryptoCurrency? fallbackCurrency,
1311
- ) async {
1312
- String? amountOverride;
1313
- final contract = paymentRequest.contractAddress;
1314
- if (contract != null && contract.isNotEmpty) {
1315
- final walletType = widget.sendViewModel.wallet.type;
1316
- final lookupType = evm != null && isEVMCompatibleChain(walletType)
1317
- ? (evm!.getWalletTypeByChainId(_currentEvmChainIdOrMainnet()) ?? walletType)
1318
- : walletType;
1319
- final token = await TokenUtilities.findTokenByAddress(
1320
- walletType: lookupType,
1321
- address: contract,
1322
- );
1323
- if (!mounted) {
1324
- return;
1325
- }
1326
-
1327
- if (token == null) {
1328
- final rerouted = await _rerouteChainlessContractPayment(paymentRequest);
1329
- if (rerouted || !mounted) {
1330
- return;
1331
- }
1332
- _showUnsupportedTokenAlert();
1333
- return;
1334
- }
1335
- await widget.sendViewModel.fetchTokenForContractAddress(contract, walletType: lookupType);
1336
- amountOverride = paymentRequest.resolveTokenAmount(token);
1337
- } else if (fallbackCurrency != null) {
1338
- widget.sendViewModel.setSelectedCryptoCurrency(fallbackCurrency.title);
1339
- if (paymentRequest.amount.isEmpty) {
1340
- widget.sendViewModel.outputs[_selectedOutput].setCryptoAmount("");
1341
- _amountControllers[_selectedOutput].clear();
1342
- }
1343
- }
1344
- if (!mounted) {
1345
- return;
1346
- }
1347
- _applyPaymentRequest(paymentRequest, amountOverride: amountOverride);
1348
- }
1349
-
1350
- Future<bool> _rerouteChainlessContractPayment(PaymentRequest paymentRequest) async {
1351
- if (evm == null || paymentRequest.chainId != null) {
1075
+ Future<bool> _handleIfOpenCryptoPay(String input) async {
1076
+ if (!OpenCryptoPayService.isOpenCryptoPayQR(input) ||
1077
+ widget.sendViewModel.selectedCryptoCurrency == CryptoCurrency.btcln) {
1078
return false;
1079
}
1080
1355
- if (paymentRequest.scheme.toLowerCase() != "ethereum") {
1356
- return false;
1357
- }
1358
-
1359
- final contract = paymentRequest.contractAddress;
1360
- if (contract == null || contract.isEmpty) {
1361
- return false;
1081
+ if (_selectedOutput != 0) {
1082
+ setState(() => _selectedOutput = 0);
1083
}
1084
1364
- final currentChainId = isEVMCompatibleChain(widget.sendViewModel.wallet.type)
1365
- ? _currentEvmChainIdOrMainnet()
1366
- : null;
1367
-
1368
- // QRs from old app versions omit the chainId on mainnet, so a contract the current
1369
- // network does not know may still belong to another EVM network
1370
- final chainId = await TokenUtilities.findEvmChainIdForContract(
1371
- contract,
1372
- excludingChainId: currentChainId,
1373
- );
1374
- if (chainId == null || !mounted) {
1375
- return false;
1376
- }
1377
-
1378
- final targetChain = evm!.getChainInfoByChainId(chainId);
1379
- if (targetChain == null) {
1380
- return false;
1085
+ final request = await widget.sendViewModel.getOpenCryptoPayRequest(input);
1086
+ if (request != null && mounted) {
1087
+ _applyPaymentRequest(request);
1088
}
1382
-
1383
- printV("chainless contract payment rerouted to chainId $chainId");
1384
- await _handleEvmNetworkFlow(targetChain, paymentRequest);
1089
return true;
1090
}
1091
1388
- Future<void> _completeWalletSwitch(
1389
- WalletInfo wallet,
1390
- PaymentFlowResult result,
1391
- PaymentRequest paymentRequest,
1392
- ) async {
1393
- widget.walletSwitcherViewModel.selectWallet(wallet);
1394
- final success = await widget.walletSwitcherViewModel.switchToSelectedWallet();
1395
- if (!success) {
1396
- _showNetworkSwitchFailedAlert();
1397
- return;
1398
- }
1399
- if (!mounted) {
1400
- return;
1401
- }
1092
+ Future<void> _runAnyPayFlow({String? rawInput, PaymentRequest? paymentRequest}) async {
1093
+ assert(rawInput != null || paymentRequest != null);
1094
1403
- bool completedFlow = false;
1404
- WidgetsBinding.instance.addPostFrameCallback((_) {
1405
- if (!mounted || completedFlow) {
1406
- return;
1407
- }
1408
- showModalBottomSheet<void>(
1409
- context: context,
1410
- isDismissible: false,
1411
- builder: (BuildContext sheetContext) {
1412
- if (completedFlow) {
1413
- WidgetsBinding.instance.addPostFrameCallback((_) {
1414
- if (Navigator.canPop(sheetContext)) {
1415
- Navigator.of(sheetContext).pop();
1416
- }
1417
- });
1418
- return const SizedBox.shrink();
1419
- }
1420
- loadingBottomSheetContext = sheetContext;
1421
- return LoadingBottomSheet(titleText: S.of(sheetContext).loading_your_wallet);
1422
- },
1423
- );
1424
- });
1425
-
1426
- bool chainSwitchFailed = false;
1427
- try {
1428
- if (evm != null &&
1429
- isEVMCompatibleChain(widget.sendViewModel.wallet.type) &&
1430
- result.chainId != null) {
1431
- final appStore = getIt.get<AppStore>();
1432
- final node = appStore.settingsStore
1433
- .getCurrentNode(widget.sendViewModel.wallet.type, chainId: result.chainId);
1434
- await evm!.selectChain(widget.sendViewModel.wallet, result.chainId!, node: node);
1435
- }
1436
- } catch (e, s) {
1437
- chainSwitchFailed = true;
1438
- printV("completeWalletSwitch failed: $e\n$s");
1439
- }
1440
-
1441
- if (!chainSwitchFailed) {
1442
- try {
1443
- await widget.sendViewModel.wallet.updateBalance();
1444
- } catch (e) {
1445
- printV("balance refresh after network switch failed: $e");
1446
- }
1447
- }
1448
-
1449
- completedFlow = true;
1450
- if (loadingBottomSheetContext != null &&
1451
- loadingBottomSheetContext!.mounted &&
1452
- Navigator.canPop(loadingBottomSheetContext!)) {
1453
- Navigator.of(loadingBottomSheetContext!).pop();
1454
- }
1455
- loadingBottomSheetContext = null;
1095
+ final evaluation = rawInput != null
1096
+ ? await widget.anyPayService.evaluateRawInput(rawInput)
1097
+ : await widget.anyPayService.evaluatePaymentRequest(paymentRequest!);
1098
1099
if (!mounted) {
1100
return;
1101
}
1102
1461
- if (chainSwitchFailed) {
1462
- _showNetworkSwitchFailedAlert();
1103
+ if (await _handleIfOpenCryptoPay(evaluation.request.rawInput)) {
1104
return;
1105
}
1106
1466
- await _applyPaymentSelectingCurrency(
1467
- paymentRequest,
1468
- result.addressDetectionResult?.detectedCurrency,
1469
- );
1107
+ await _handleEvaluation(evaluation);
1108
}
1109
1472
- void _showNetworkSwitchFailedAlert() {
1473
- if (!mounted) {
1110
+ Future<void> _runAnyPayPaste(String pasted) async {
1111
+ if (await _handleIfOpenCryptoPay(pasted)) {
1112
return;
1113
}
1114
1477
- showPopUp<void>(
1478
- context: context,
1479
- builder: (context) => AlertWithOneAction(
1480
- alertTitle: S.of(context).error,
1481
- alertContent: S.of(context).network_switch_failed,
1482
- buttonText: S.of(context).ok,
1483
- buttonAction: () => Navigator.of(context).pop(),
1484
- ),
1115
+ final parsed = PaymentRequest.fromString(pasted);
1116
+ final parsedAmount = parsed.amount == "0" ? "" : parsed.amount;
1117
+ final merged = parsed.copyWith(
1118
+ amount: parsedAmount.isNotEmpty ? parsedAmount : _amountControllers[_selectedOutput].text,
1119
+ note: parsed.note.isNotEmpty ? parsed.note : _memoControllers[_selectedOutput].text,
1120
);
1486
- }
1121
1488
- void _showUnsupportedTokenAlert() {
1489
- if (!mounted) {
1490
- return;
1491
- }
1492
-
1493
- showPopUp<void>(
1494
- context: context,
1495
- builder: (context) => AlertWithOneAction(
1496
- alertTitle: S.of(context).error,
1497
- alertContent: S.of(context).unsupported_token_requested,
1498
- buttonText: S.of(context).ok,
1499
- buttonAction: () => Navigator.of(context).pop(),
1500
- ),
1501
- );
1122
+ await _runAnyPayFlow(paymentRequest: merged);
1123
}
1124
1504
- Future<void> _showEvmNetworkPicker(
1505
- PaymentRequest paymentRequest,
1506
- WalletType? fixedNetwork,
1507
- ) async {
1508
- if (!mounted || evm == null || fixedNetwork == null || !isEVMCompatibleChain(fixedNetwork)) {
1125
+ Future<void> _handleEvaluation(
1126
+ AnyPayEvaluation evaluation, {
1127
+ CryptoCurrency? fallbackCurrency,
1128
+ }) async {
1129
+ final evaluationResult = await _anyPayFlow.handleEvaluation(
1130
+ context,
1131
+ evaluation,
1132
+ fallbackCurrency: fallbackCurrency,
1133
+ );
1134
+ if (evaluationResult == null || !mounted) {
1135
return;
1136
}
1137
1512
- final chains = evm!.getAllChains();
1513
- final networks = chains.map((chain) {
1514
- final walletType = evm!.getWalletTypeByChainId(chain.chainId) ?? fixedNetwork;
1515
- return RecipientNetworkItem(
1516
- chainId: chain.chainId,
1517
- name: chain.name,
1518
- iconPath: getCryptoCurrencyIconForWalletListItem(walletType, chainId: chain.chainId),
1519
- chainBadgeIconPath: symbolIconPathForWalletType(walletType),
1138
+ try {
1139
+ await _applyIntent(
1140
+ evaluationResult.request,
1141
+ token: evaluationResult.token,
1142
+ fallbackCurrency: evaluationResult.fallbackCurrency,
1143
+ amountOverride: evaluationResult.amountOverride,
1144
);
1521
- }).toList();
1522
-
1523
- final selectedChainId =
1524
- await EvmAddressDetectedSheet.show(context: context, networks: networks);
1525
- if (!mounted || selectedChainId == null) {
1526
- return;
1145
+ } catch (e) {
1146
+ printV("Payment flow error: $e");
1147
+ _applyPaymentRequest(evaluationResult.request.paymentRequest);
1148
}
1528
-
1529
- final target = chains.firstWhere(
1530
- (chain) => chain.chainId == selectedChainId,
1531
- orElse: () => chains.first,
1532
- );
1533
- await _handleEvmNetworkFlow(target, paymentRequest);
1149
}
1150
1536
- Future<void> _handleEvmNetworkFlow(ChainInfo target, PaymentRequest paymentRequest) async {
1537
- if (evm == null || !mounted) {
1538
- return;
1539
- }
1540
-
1541
- final targetType = evm!.getWalletTypeByChainId(target.chainId);
1542
- if (targetType == null) {
1543
- _showUnsupportedNetworkAlert(target.chainId);
1544
- return;
1545
- }
1546
-
1547
- final detection = AddressDetectionResult(
1548
- address: paymentRequest.address,
1549
- detectedWalletType: targetType,
1550
- detectedCurrency: target.currency,
1551
- chainId: target.chainId,
1552
- amount: paymentRequest.amount,
1553
- note: paymentRequest.note,
1554
- scheme: "",
1555
- isValid: true,
1556
- );
1557
- widget.paymentViewModel.applyManualEvmSelection(detection);
1558
-
1559
- final currentType = widget.sendViewModel.wallet.type;
1560
- final currentChainId = isEVMCompatibleChain(currentType) ? _currentEvmChainIdOrMainnet() : null;
1561
- if (currentChainId != null && target.chainId == currentChainId) {
1562
- await _applyPaymentSelectingCurrency(paymentRequest, target.currency);
1563
- return;
1151
+ Future<void> _applyIntent(
1152
+ AnyPayRequest request, {
1153
+ CryptoCurrency? token,
1154
+ CryptoCurrency? fallbackCurrency,
1155
+ String? amountOverride,
1156
+ }) async {
1157
+ final currency = token ?? fallbackCurrency;
1158
+ if (currency != null) {
1159
+ widget.sendViewModel.applyAnyPayCurrency(currency);
1160
}
1161
1566
- final compatibleWallets = await widget.paymentViewModel.getWalletsByType(targetType);
1567
- if (!mounted) {
1568
- return;
1162
+ if (token == null && fallbackCurrency != null && request.amount.isEmpty) {
1163
+ widget.sendViewModel.outputs[_selectedOutput].setCryptoAmount("");
1164
+ _amountControllers[_selectedOutput].clear();
1165
}
1166
1571
- final result = PaymentFlowResult.evmNetworkSelection(
1572
- detection,
1573
- compatibleWallets: compatibleWallets,
1574
- wallet: compatibleWallets.isNotEmpty ? compatibleWallets.first : null,
1575
- );
1576
-
1577
- await _showPaymentConfirmation(
1578
- widget.paymentViewModel,
1579
- widget.walletSwitcherViewModel,
1580
- paymentRequest,
1581
- result,
1582
- );
1167
+ _applyPaymentRequest(request.paymentRequest, amountOverride: amountOverride);
1168
}
1169
1170
void _applyPaymentRequest(PaymentRequest paymentRequest, {String? amountOverride}) {
1189
_applyNote(paymentRequest.note, _selectedOutput);
1190
}
1191
1607
- Future<void> _handleSwapFlow(
1608
- PaymentFlowResult result,
1609
- BuildContext presentContext,
1610
- PaymentRequest paymentRequest,
1611
- ) async {
1612
- if (!mounted) {
1613
- return;
1614
- }
1615
-
1616
- final destWalletType = result.walletType;
1617
- if (destWalletType == null) {
1618
- return;
1619
- }
1620
-
1621
- CryptoCurrency? resolvedToken;
1622
- final contract = paymentRequest.contractAddress;
1623
- if (contract != null && contract.isNotEmpty) {
1624
- resolvedToken = await TokenUtilities.findTokenByAddress(
1625
- walletType: destWalletType,
1626
- address: contract,
1627
- );
1628
- }
1629
- if (!mounted || !presentContext.mounted) {
1630
- return;
1631
- }
1632
-
1633
- if (contract != null && contract.isNotEmpty && resolvedToken == null) {
1634
- _showUnsupportedTokenAlert();
1635
- return;
1636
- }
1637
-
1638
- final receiveCurrency = resolvedToken ??
1639
- result.detectedCurrency ??
1640
- walletTypeToCryptoCurrency(destWalletType, chainId: result.chainId);
1641
-
1642
- final receiveAmountValue = resolvedToken != null
1643
- ? paymentRequest.resolveTokenAmount(resolvedToken)
1644
- : (paymentRequest.amount.isNotEmpty ? paymentRequest.amount : null);
1645
- final receiveAmount =
1646
- receiveAmountValue != null ? Money.tryParse(receiveAmountValue, receiveCurrency) : null;
1647
-
1648
- final isFiatDisabled = widget.sendViewModel.isFiatDisabled;
1649
- final depositBalanceByAsset = <CryptoCurrency, CurrencyPickerBalance>{
1650
- for (final r in widget.sendViewModel.balanceViewModel.formattedBalances)
1651
- r.asset: CurrencyPickerBalance(
1652
- amount: "${r.availableBalance} ${r.asset.title}",
1653
- fiat: isFiatDisabled ? null : "${r.fiatAvailableBalanceRaw} ${r.fiatCurrency?.symbol}",
1654
- fiatValue: isFiatDisabled ? null : double.tryParse(r.fiatAvailableBalanceRaw),
1655
- ),
1656
- };
1657
-
1658
- final page = NewSwapPage(
1659
- getIt.get<ExchangeViewModel>(),
1660
- widget.authService,
1661
- getIt.get<AddressResolverService>(),
1662
- null,
1663
- walletSwitcherViewModel: widget.walletSwitcherViewModel,
1664
- fromSend: SwapFromSendArgs(
1665
- recipientAddress: result.addressDetectionResult?.address ?? "",
1666
- receiveCurrency: receiveCurrency,
1667
- targetWalletType: destWalletType,
1668
- depositBalanceByAsset: depositBalanceByAsset,
1669
- receiveAmount: receiveAmount,
1670
- ),
1671
- );
1672
- await Navigator.of(presentContext).push<void>(
1673
- CupertinoPageRoute(
1674
- builder: (context) => Material(
1675
- color: Theme.of(context).colorScheme.surface,
1676
- child: SafeArea(bottom: false, child: page),
1677
- ),
1678
- ),
1679
- );
1680
- }
1681
-
1192
void showErrorValidationAlert(BuildContext context) {
1193
int emptyAddressIndex = -1;
1194
for (int i = 0; i < widget.sendViewModel.outputs.length; i++) {