| 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 | } |