dev
dart 391 lines 13.1 KB
Raw
1 import "package:cake_wallet/core/address_resolver/address_resolver_service.dart";
2 import "package:cake_wallet/core/anypay/anypay_models.dart";
3 import "package:cake_wallet/core/anypay/anypay_service.dart";
4 import "package:cake_wallet/core/auth_service.dart";
5 import "package:cake_wallet/di.dart";
6 import "package:cake_wallet/evm/evm.dart";
7 import "package:cake_wallet/generated/i18n.dart";
8 import "package:cake_wallet/new-ui/pages/swap_page.dart";
9 import "package:cake_wallet/new-ui/widgets/anypay/evm_address_detected_sheet.dart";
10 import "package:cake_wallet/new-ui/widgets/anypay/network_decision_page.dart";
11 import "package:cake_wallet/new-ui/widgets/anypay/select_recipient_network_sheet.dart";
12 import "package:cake_wallet/new-ui/widgets/anypay/switch_network_wallet_page.dart";
13 import "package:cake_wallet/new-ui/widgets/swap_page/swap_from_send_args.dart";
14 import "package:cake_wallet/reactions/wallet_connect.dart";
15 import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
16 import "package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart";
17 import "package:cake_wallet/utils/show_pop_up.dart";
18 import "package:cake_wallet/view_model/exchange/exchange_view_model.dart";
19 import "package:cake_wallet/view_model/send/send_view_model.dart";
20 import "package:cake_wallet/view_model/wallet_switcher_view_model.dart";
21 import "package:cw_core/crypto_currency.dart";
22 import "package:cw_core/currency_for_wallet_type.dart";
23 import "package:cw_core/unspent_coin_type.dart";
24 import "package:cw_core/utils/print_verbose.dart";
25 import "package:cw_core/wallet_info.dart";
26 import "package:flutter/cupertino.dart";
27 import "package:flutter/material.dart";
28
29 class AnyPayFormFill {
30 const AnyPayFormFill({
31 required this.request,
32 this.token,
33 this.fallbackCurrency,
34 this.amountOverride,
35 });
36
37 final AnyPayRequest request;
38 final CryptoCurrency? token;
39 final CryptoCurrency? fallbackCurrency;
40 final String? amountOverride;
41 }
42
43 class AnyPayFlow {
44 AnyPayFlow({
45 required this.anyPayService,
46 required this.sendViewModel,
47 required this.authService,
48 required this.walletSwitcherViewModel,
49 });
50
51 final AnyPayService anyPayService;
52 final SendViewModel sendViewModel;
53 final AuthService authService;
54 final WalletSwitcherViewModel walletSwitcherViewModel;
55
56 bool _isSwitchingWallet = false;
57
58 bool get isSwitchingWallet => _isSwitchingWallet;
59
60 Future<AnyPayFormFill?> handleEvaluation(
61 BuildContext context,
62 AnyPayEvaluation evaluation, {
63 CryptoCurrency? fallbackCurrency,
64 }) async {
65 final request = evaluation.request;
66
67 if (!request.rawInput.toLowerCase().startsWith("ethereum:") &&
68 (request.rawInput.contains("@") || request.address.contains("@"))) {
69 return null;
70 }
71
72 try {
73 return await _handleDecision(context, evaluation, fallbackCurrency);
74 } catch (e) {
75 printV("Payment flow error: $e");
76 return AnyPayFormFill(request: request);
77 }
78 }
79
80 Future<AnyPayFormFill?> _handleDecision(
81 BuildContext context,
82 AnyPayEvaluation evaluation,
83 CryptoCurrency? fallbackCurrency,
84 ) async {
85 final request = evaluation.request;
86
87 switch (evaluation.decision) {
88 case final AnyPayApplyToCurrentWallet decision:
89 if (request.isLightning) {
90 _enterLightningMode();
91 }
92 return AnyPayFormFill(
93 request: request,
94 token: decision.token,
95 fallbackCurrency: fallbackCurrency ?? decision.fallbackCurrency,
96 amountOverride: decision.amountOverride,
97 );
98 case AnyPayEvmNetworkChoice():
99 return _pickEvmNetwork(context, request);
100 case final AnyPayCrossChainPayment decision:
101 return _handleCrossChain(context, request, decision, fallbackCurrency);
102 case final AnyPayUnsupportedNetwork decision:
103 _showError(
104 context,
105 S.of(context).unsupported_network_requested(decision.chainId.toString()),
106 );
107 return null;
108 case AnyPayUnsupportedToken():
109 _showError(context, S.of(context).unsupported_token_requested);
110 return null;
111 case AnyPayEmptyInput():
112 return null;
113 }
114 }
115
116 Future<AnyPayFormFill?> _pickEvmNetwork(BuildContext context, AnyPayRequest request) async {
117 final chains = evm!.getAllChains();
118 final networks = chains.map((chain) {
119 final walletType = evm!.getWalletTypeByChainId(chain.chainId) ?? sendViewModel.wallet.type;
120 return RecipientNetworkItem(
121 chainId: chain.chainId,
122 name: chain.name,
123 iconPath: getCryptoCurrencyIconForWalletListItem(walletType, chainId: chain.chainId),
124 chainBadgeIconPath: symbolIconPathForWalletType(walletType),
125 );
126 }).toList();
127
128 final selectedChainId =
129 await EvmAddressDetectedSheet.show(context: context, networks: networks);
130 if (!context.mounted || selectedChainId == null) {
131 return null;
132 }
133
134 final target = chains.firstWhere(
135 (chain) => chain.chainId == selectedChainId,
136 orElse: () => chains.first,
137 );
138
139 final evaluation = await anyPayService.evaluateForEvmChain(request, target.chainId);
140 if (!context.mounted) {
141 return null;
142 }
143 return handleEvaluation(context, evaluation, fallbackCurrency: target.currency);
144 }
145
146 Future<AnyPayFormFill?> _handleCrossChain(
147 BuildContext context,
148 AnyPayRequest request,
149 AnyPayCrossChainPayment decision,
150 CryptoCurrency? fallbackCurrency,
151 ) async {
152 final destinationType = decision.targetWalletType;
153 final isEvmTarget = isEVMCompatibleChain(destinationType);
154 final destinationNetworkName = networkDisplayName(destinationType, decision.targetChainId);
155 final destinationNetworkIcon = symbolIconPathForWalletType(destinationType) ?? "";
156
157 if (!decision.canSwap && !decision.hasCompatibleWallet) {
158 _showError(
159 context,
160 S.of(context).swap_unavailable_needs_network_wallet(destinationNetworkName),
161 );
162 return null;
163 }
164
165 final currentType = sendViewModel.wallet.type;
166 final currentChainId = isEVMCompatibleChain(currentType) ? _currentEvmChainId() : null;
167 final currentNetworkName = networkDisplayName(currentType, currentChainId);
168 final currentNetworkIcon = symbolIconPathForWalletType(currentType) ?? "";
169
170 if (!decision.hasCompatibleWallet) {
171 final swapConfirmTitle = isEvmTarget
172 ? S.of(context).swap_from_network(currentNetworkName)
173 : S.of(context).network_address_detected(destinationNetworkName);
174 final swapConfirmPrimary = isEvmTarget
175 ? S.of(context).continue_to_swap
176 : S.of(context).swap_from_network(currentNetworkName);
177
178 await Navigator.of(context).push<void>(
179 CupertinoPageRoute(
180 builder: (pageContext) => NetworkDecisionPage(
181 title: swapConfirmTitle,
182 description: S.of(context).swap_from_network_description(
183 destinationNetworkName,
184 currentNetworkName,
185 ),
186 destinationIconPath: destinationNetworkIcon,
187 currentIconPath: currentNetworkIcon,
188 primaryText: swapConfirmPrimary,
189 primaryIconPath: isEvmTarget ? null : "assets/new-ui/swap_arrows.svg",
190 onPrimary: () => _openSwap(pageContext, request, decision),
191 secondaryText: S.of(context).cancel,
192 ),
193 ),
194 );
195 return null;
196 }
197
198 final decisionTitle = isEvmTarget
199 ? S.of(context).send_to_network(destinationNetworkName)
200 : S.of(context).network_address_detected(destinationNetworkName);
201
202 final selectedWallet = await Navigator.of(context).push<WalletInfo>(
203 CupertinoPageRoute(
204 builder: (pageContext) => NetworkDecisionPage(
205 title: decisionTitle,
206 description: decision.canSwap
207 ? S.of(context).send_to_network_description(
208 destinationNetworkName,
209 currentNetworkName,
210 )
211 : S.of(context).swap_unavailable_switch_to_network(destinationNetworkName),
212 destinationIconPath: destinationNetworkIcon,
213 primaryText: S.of(context).switch_to_x_wallet(destinationNetworkName),
214 primaryIconPath: "assets/new-ui/wallet_filled.svg",
215 onPrimary: () => _pickDestinationWallet(pageContext, decision, destinationNetworkName),
216 secondaryText: decision.canSwap
217 ? S.of(context).swap_from_network(currentNetworkName)
218 : S.of(context).cancel,
219 secondaryIconPath: decision.canSwap ? "assets/new-ui/swap_arrows.svg" : null,
220 onSecondary: decision.canSwap ? () => _openSwap(pageContext, request, decision) : null,
221 ),
222 ),
223 );
224
225 if (selectedWallet == null || !context.mounted) {
226 return null;
227 }
228
229 return _switchWallet(context, selectedWallet, request, decision, fallbackCurrency);
230 }
231
232 Future<void> _pickDestinationWallet(
233 BuildContext pageContext,
234 AnyPayCrossChainPayment decision,
235 String networkName,
236 ) async {
237 WalletInfo? destinationWalletInfo;
238 if (decision.wallets.length > 1) {
239 destinationWalletInfo = await SwitchNetworkWalletPage.push(
240 context: pageContext,
241 networkName: networkName,
242 targetIconPath: symbolIconPathForWalletType(decision.targetWalletType) ?? "",
243 wallets: decision.wallets,
244 );
245 } else {
246 destinationWalletInfo = decision.wallets.first;
247 }
248
249 if (destinationWalletInfo == null || !pageContext.mounted) {
250 return;
251 }
252
253 Navigator.of(pageContext).pop(destinationWalletInfo);
254 }
255
256 Future<AnyPayFormFill?> _switchWallet(
257 BuildContext context,
258 WalletInfo walletInfo,
259 AnyPayRequest request,
260 AnyPayCrossChainPayment decision,
261 CryptoCurrency? fallbackCurrency,
262 ) async {
263 if (request.isLightning) {
264 _enterLightningMode();
265 }
266
267 _isSwitchingWallet = true;
268 BuildContext? loadingSheetContext;
269 bool completedFlow = false;
270 WidgetsBinding.instance.addPostFrameCallback((_) {
271 if (!context.mounted || completedFlow) {
272 return;
273 }
274 showModalBottomSheet<void>(
275 context: context,
276 isDismissible: false,
277 builder: (sheetContext) {
278 if (completedFlow) {
279 WidgetsBinding.instance.addPostFrameCallback((_) {
280 if (Navigator.canPop(sheetContext)) {
281 Navigator.of(sheetContext).pop();
282 }
283 });
284 return const SizedBox.shrink();
285 }
286 loadingSheetContext = sheetContext;
287 return LoadingBottomSheet(titleText: S.of(sheetContext).loading_your_wallet);
288 },
289 );
290 });
291
292 bool success = false;
293 try {
294 success = await anyPayService.switchWalletForPayment(
295 walletInfo,
296 chainId: decision.targetChainId,
297 );
298 } finally {
299 _isSwitchingWallet = false;
300 completedFlow = true;
301 if (loadingSheetContext != null &&
302 loadingSheetContext!.mounted &&
303 Navigator.canPop(loadingSheetContext!)) {
304 Navigator.of(loadingSheetContext!).pop();
305 }
306 loadingSheetContext = null;
307 }
308
309 if (!context.mounted) {
310 return null;
311 }
312
313 if (!success) {
314 _showError(context, S.of(context).network_switch_failed);
315 return null;
316 }
317
318 return AnyPayFormFill(
319 request: request,
320 token: decision.token,
321 fallbackCurrency: fallbackCurrency ?? request.detection.detectedCurrency,
322 amountOverride: decision.amountOverride,
323 );
324 }
325
326 Future<void> _openSwap(
327 BuildContext presentContext,
328 AnyPayRequest request,
329 AnyPayCrossChainPayment decision,
330 ) async {
331 if (!presentContext.mounted) {
332 return;
333 }
334
335 if (request.hasContract && decision.token == null) {
336 _showError(presentContext, S.of(presentContext).unsupported_token_requested);
337 return;
338 }
339
340 final intent = anyPayService.buildSwapIntent(
341 request,
342 targetWalletType: decision.targetWalletType,
343 targetChainId: decision.targetChainId,
344 token: decision.token,
345 );
346
347 final page = NewSwapPage(
348 getIt.get<ExchangeViewModel>(),
349 authService,
350 getIt.get<AddressResolverService>(),
351 null,
352 walletSwitcherViewModel: walletSwitcherViewModel,
353 fromSend: SwapFromSendArgs.fromIntent(intent),
354 balanceViewModel: sendViewModel.balanceViewModel,
355 );
356 await Navigator.of(presentContext).push<void>(
357 CupertinoPageRoute(
358 builder: (context) => Material(
359 color: Theme.of(context).colorScheme.surface,
360 child: SafeArea(bottom: false, child: page),
361 ),
362 ),
363 );
364 }
365
366 void _showError(BuildContext context, String message) {
367 if (!context.mounted) {
368 return;
369 }
370
371 showPopUp<void>(
372 context: context,
373 builder: (context) => AlertWithOneAction(
374 alertTitle: S.of(context).error,
375 alertContent: message,
376 buttonText: S.of(context).ok,
377 buttonAction: () => Navigator.of(context).pop(),
378 ),
379 );
380 }
381
382 void _enterLightningMode() {
383 sendViewModel.selectedCryptoCurrency = CryptoCurrency.btcln;
384 sendViewModel.coinTypeToSpendFrom = UnspentCoinType.lightning;
385 }
386
387 int _currentEvmChainId() {
388 final wallet = sendViewModel.wallet;
389 return evm!.getSelectedChainId(wallet) ?? evm!.getChainIdByWalletType(wallet.type);
390 }
391 }