diff --git a/features/approval/api/src/main/java/com/tangem/features/approval/api/SelectApprovalTypeComponent.kt b/features/approval/api/src/main/java/com/tangem/features/approval/api/SelectApprovalTypeComponent.kt index 193d11d9d8..98eac8760e 100644 --- a/features/approval/api/src/main/java/com/tangem/features/approval/api/SelectApprovalTypeComponent.kt +++ b/features/approval/api/src/main/java/com/tangem/features/approval/api/SelectApprovalTypeComponent.kt @@ -25,11 +25,12 @@ interface SelectApprovalTypeComponent : ComposableBottomSheetComponent { val cryptoCurrencyStatus: CryptoCurrencyStatus, val amountFooter: TextReference, val initialApproveType: ApproveType = ApproveType.LIMITED, + val spenderAddress: String, val callback: Callback, ) interface Callback { - fun onApproveTypeSelected(approveType: ApproveType) + fun onApproveTypeSelected(spenderAddress: String, approveType: ApproveType) fun onCancelClick() } diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/SelectApprovalTypeModel.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/SelectApprovalTypeModel.kt index e7e063601a..737861647a 100644 --- a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/SelectApprovalTypeModel.kt +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/SelectApprovalTypeModel.kt @@ -43,7 +43,7 @@ internal class SelectApprovalTypeModel @Inject constructor( } fun onConfirmClick() { - params.callback.onApproveTypeSelected(uiState.value.approveType) + params.callback.onApproveTypeSelected(params.spenderAddress, uiState.value.approveType) } fun onCancelClick() { diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index 00bc4cb0a8..fa2adfc216 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -53,6 +53,9 @@ dependencies { implementation(projects.domain.balanceHiding) implementation(projects.domain.yieldSupply) + /** Common modules */ + implementation(projects.common.ui) + /** Core modules */ implementation(projects.core.configToggles) implementation(projects.core.utils) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 059dd0647c..362e35f4e1 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -16,6 +16,7 @@ import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldS import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format @@ -252,7 +253,7 @@ internal class SwapInteractorImpl @Inject constructor( } } - @Suppress("LongMethod") + @Suppress("LongMethod", "CyclomaticComplexMethod") private suspend fun manageDex( fromSwapCurrencyStatus: SwapCurrencyStatus, toSwapCurrencyStatus: SwapCurrencyStatus, @@ -271,7 +272,7 @@ internal class SwapInteractorImpl @Inject constructor( ) } - val maybeQuotes = repository.findBestQuote( + val maybeQuote = repository.findBestQuote( userWallet = fromSwapCurrencyStatus.userWallet, fromContractAddress = fromSwapCurrencyStatus.currency.getContractAddress(), fromNetwork = fromSwapCurrencyStatus.currency.network.rawId, @@ -284,7 +285,7 @@ internal class SwapInteractorImpl @Inject constructor( rateType = RateType.FLOAT, ) - if (maybeQuotes.getOrNull()?.txType == ExpressTxType.SEND) { + if (maybeQuote.getOrNull()?.txType == ExpressTxType.SEND) { return manageCex( fromSwapCurrencyStatus = fromSwapCurrencyStatus, toSwapCurrencyStatus = toSwapCurrencyStatus, @@ -295,28 +296,32 @@ internal class SwapInteractorImpl @Inject constructor( } val fromTokenAddress = getTokenAddress(fromSwapCurrencyStatus.currency) + + // TODO CHECK YIELD APPROVE val isYieldSwap = fromSwapCurrencyStatus.isYieldSwapActive && fromSwapCurrencyStatus.currency is CryptoCurrency.Token - val isAllowedToSpend = if (isYieldSwap) { - maybeQuotes.isRight() && - fromSwapCurrencyStatus.status.value.yieldSupplyStatus?.isAllowedToSpend == true - } else { - maybeQuotes.fold( - ifRight = { quotes -> - quotes.allowanceContract?.let { allowanceContract -> - getAllowanceInfoUseCase( - userWalletId = fromSwapCurrencyStatus.userWalletId, - cryptoCurrency = fromSwapCurrencyStatus.currency, - spenderAddress = allowanceContract, - requiredAmount = amount.value, - ).getOrNull() is AllowanceInfo.Enough - } != false - }, - ifLeft = { false }, + + val spenderAddress = if (isYieldSwap) { + yieldModuleAddressProvider.getOrFetch( + userWalletId = fromSwapCurrencyStatus.userWalletId, + network = fromSwapCurrencyStatus.currency.network, ) + } else { + maybeQuote.getOrNull()?.allowanceContract } - if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { + val allowanceInfo = spenderAddress?.let { allowanceContract -> + getAllowanceInfoUseCase( + userWalletId = fromSwapCurrencyStatus.userWalletId, + cryptoCurrency = fromSwapCurrencyStatus.currency, + spenderAddress = allowanceContract, + requiredAmount = amount.value, + ).getOrNull() + } ?: AllowanceInfo.Enough(allowance = BigDecimal.ZERO) + + if (allowanceInfo is AllowanceInfo.Enough && + allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress) + ) { allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) cryptoCurrencyBalanceFetcher( userWalletId = fromSwapCurrencyStatus.userWalletId, @@ -324,7 +329,21 @@ internal class SwapInteractorImpl @Inject constructor( ) } val isBalanceWithoutFeeEnough = isBalanceEnough(fromSwapCurrencyStatus, amount, null) - val quoteAllowanceContract = maybeQuotes.getOrNull()?.allowanceContract + val isIntegratedApproveActive = swapFeatureToggles.isSwapIntegratedApproveEnabled + val isAllowanceSatisfied = if (isIntegratedApproveActive) { + allowanceInfo !is AllowanceInfo.ResetNeeded + } else { + allowanceInfo is AllowanceInfo.Enough + } + // For yield swaps the on-chain allowance is not sufficient on its own: spending also + // requires the yield-module proxy approval (yieldSupplyStatus.isAllowedToSpend). + // For regular swaps a failed quote must not proceed to exchange-data loading. + val isAllowedToSpend = if (isYieldSwap) { + isAllowanceSatisfied && + fromSwapCurrencyStatus.status.value.yieldSupplyStatus?.isAllowedToSpend == true + } else { + isAllowanceSatisfied && maybeQuote.isRight() + } return if (isAllowedToSpend && isBalanceWithoutFeeEnough) { provider to loadDexSwapDataNoFee( provider = provider, @@ -332,7 +351,8 @@ internal class SwapInteractorImpl @Inject constructor( toSwapCurrencyStatus = toSwapCurrencyStatus, amount = amount, expressOperationType = expressOperationType, - quoteAllowanceContract = quoteAllowanceContract, + allowanceInfo = allowanceInfo, + spenderAddress = spenderAddress, ) } else { val quoteBalanceStatus = if (isBalanceWithoutFeeEnough) { @@ -342,7 +362,7 @@ internal class SwapInteractorImpl @Inject constructor( } provider to getQuotesState( provider = provider, - quoteDataModel = maybeQuotes, + quoteDataModel = maybeQuote, amount = amount, fromSwapCurrencyStatus = fromSwapCurrencyStatus, toSwapCurrencyStatus = toSwapCurrencyStatus, @@ -395,7 +415,8 @@ internal class SwapInteractorImpl @Inject constructor( toSwapCurrencyStatus = toSwapCurrencyStatus, amount = amount, expressOperationType = expressOperationType, - quoteAllowanceContract = maybeQuotes.getOrNull()?.allowanceContract, + allowanceInfo = null, + spenderAddress = null, ) } else { provider to getQuotesState( @@ -1594,7 +1615,8 @@ internal class SwapInteractorImpl @Inject constructor( toSwapCurrencyStatus: SwapCurrencyStatus, amount: SwapAmount, expressOperationType: ExpressOperationType, - quoteAllowanceContract: String? = null, + allowanceInfo: AllowanceInfo?, + spenderAddress: String?, ): SwapState { val fromNetworkAddress = fromSwapCurrencyStatus.status.value.networkAddress val dexFromAddress = fromNetworkAddress?.defaultAddress?.value.orEmpty() @@ -1617,8 +1639,8 @@ internal class SwapInteractorImpl @Inject constructor( expressOperationType = expressOperationType, ).map { swapData -> val dexTx = swapData.transaction as? ExpressTransactionModel.DEX - if (dexTx != null && quoteAllowanceContract != null && dexTx.allowanceContract == null) { - swapData.copy(transaction = dexTx.copy(allowanceContract = quoteAllowanceContract)) + if (dexTx != null && spenderAddress != null && dexTx.allowanceContract == null) { + swapData.copy(transaction = dexTx.copy(allowanceContract = spenderAddress)) } else { swapData } @@ -1636,8 +1658,24 @@ internal class SwapInteractorImpl @Inject constructor( swapData = swapData, provider = provider, ) + val isIntegratedApprovalNeeded = swapFeatureToggles.isSwapIntegratedApproveEnabled && + allowanceInfo is AllowanceInfo.NotEnough swapState.copy( - permissionState = PermissionDataState.Empty, + permissionState = if (isIntegratedApprovalNeeded) { + PermissionDataState.PermissionSettings( + type = ApproveType.LIMITED, + spenderAddress = spenderAddress.orEmpty(), + ) + } else if (allowanceInfo is AllowanceInfo.NotEnough) { + // Integrated estimation failed earlier this session — show the legacy + // separate-approval UI so the user approves before swapping. + PermissionDataState.PermissionRequired( + isResetApproval = false, + spenderAddress = spenderAddress.orEmpty(), + ) + } else { + PermissionDataState.Empty + }, currencyCheck = manageWarnings( fromSwapCurrencyStatus = fromSwapCurrencyStatus, amount = amount, @@ -1760,13 +1798,23 @@ internal class SwapInteractorImpl @Inject constructor( cryptoCurrency = fromToken, spenderAddress = spenderAddress, requiredAmount = swapAmount.value, - ).getOrNull() + ).getOrNull() ?: return quotesLoadedState.copy(permissionState = PermissionDataState.Empty) + + val isIntegratedApprovalNeeded = swapFeatureToggles.isSwapIntegratedApproveEnabled && + allowanceInfo is AllowanceInfo.NotEnough return quotesLoadedState.copy( - permissionState = PermissionDataState.PermissionRequired( - isResetApproval = allowanceInfo is AllowanceInfo.ResetNeeded, - spenderAddress = spenderAddress, - ), + permissionState = if (isIntegratedApprovalNeeded) { + PermissionDataState.PermissionSettings( + type = ApproveType.LIMITED, + spenderAddress = spenderAddress, + ) + } else { + PermissionDataState.PermissionRequired( + isResetApproval = allowanceInfo is AllowanceInfo.ResetNeeded, + spenderAddress = spenderAddress, + ) + }, ) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 861066f760..9e41e40ec8 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap.domain.models.ui import androidx.compose.runtime.Immutable +import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.wallet.UserWallet @@ -102,6 +103,11 @@ sealed class PermissionDataState { val spenderAddress: String, ) : PermissionDataState() + data class PermissionSettings( + val type: ApproveType, + val spenderAddress: String, + ) : PermissionDataState() + object PermissionLoading : PermissionDataState() object Empty : PermissionDataState() diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt index cb68e69748..fd54279f0c 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt @@ -20,22 +20,16 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.navigation.inner.InnerRouter -import com.tangem.core.ui.R import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.parseBigDecimalOrNull import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.isHotWallet import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent -import com.tangem.feature.swap.domain.models.ui.PermissionDataState import com.tangem.feature.swap.model.SwapModel -import com.tangem.feature.swap.models.SwapPermissionUM import com.tangem.feature.swap.router.SwapRoute import com.tangem.feature.swap.ui.SwapScreen import com.tangem.feature.swap.ui.SwapSuccessScreen -import com.tangem.features.approval.api.GiveApprovalComponent +import com.tangem.features.approval.api.GiveApprovalEntryComponent import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenComponent import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.swap.SwapComponent @@ -50,7 +44,7 @@ internal class DefaultSwapComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: SwapComponent.Params, private val swapFeeSelectorBlockComponentFactory: SwapFeeSelectorBlockComponent.Factory, - private val giveApprovalComponentFactory: GiveApprovalComponent.Factory, + private val giveApprovalEntryComponentFactory: GiveApprovalEntryComponent.Factory, private val chooseTokenComponentFactory: ChooseTokenComponent.Factory, ) : SwapComponent, AppComponentContext by appComponentContext { @@ -78,12 +72,10 @@ internal class DefaultSwapComponent @AssistedInject constructor( source = model.approvalSlotNavigation, serializer = null, handleBackButton = true, - childFactory = { _, factoryContext -> - val approvalParams = getApprovalParams() - ?: error("Approval params are not available") - giveApprovalComponentFactory.create( + childFactory = { params, factoryContext -> + giveApprovalEntryComponentFactory.create( context = childByContext(factoryContext), - params = approvalParams, + params = GiveApprovalEntryComponent.Params(params), ) }, ) @@ -159,9 +151,8 @@ internal class DefaultSwapComponent @AssistedInject constructor( val isAmountEmptyOrZero = dataState.amount?.parseBigDecimalOrNull().isNullOrZero() val isInsufficientFunds = model.uiState.isInsufficientFunds val isProviderMissing = dataState.selectedProvider == null - val loadedState = dataState.getCurrentLoadedSwapState() - val isPermissionNotReady = loadedState?.permissionState !is PermissionDataState.Empty - val isSwapNotReady = !isInTransferMode && (isProviderMissing || isPermissionNotReady) + val isPermissionNotNeeded = model.isPermissionNotNeeded + val isSwapNotReady = !isInTransferMode && (isProviderMissing || !isPermissionNotNeeded) val isTangemPayWithdrawal = model.isTangemPayWithdrawal() isAmountEmptyOrZero || isInsufficientFunds || isSwapNotReady || isTangemPayWithdrawal @@ -249,34 +240,6 @@ internal class DefaultSwapComponent @AssistedInject constructor( } } - private fun getApprovalParams(): GiveApprovalComponent.Params? { - val permissionState = model.uiState.permissionUM as? SwapPermissionUM.PermissionRequired ?: return null - val fromSwapCurrencyStatus = model.dataState.fromSwapCurrencyStatus ?: return null - val feeCryptoCurrency = model.dataState.feePaidCryptoCurrency ?: return null - val providerName = model.dataState.selectedProvider?.name.orEmpty() - val isHoldToConfirm = fromSwapCurrencyStatus.userWallet.isHotWallet - - return GiveApprovalComponent.Params( - userWalletId = params.userWalletId, - cryptoCurrencyStatus = fromSwapCurrencyStatus.status, - feeCryptoCurrencyStatus = feeCryptoCurrency, - amount = model.dataState.amount.orEmpty(), - spenderAddress = permissionState.spenderAddress, - amountFooter = if (permissionState.isResetApproval) { - resourceReference(R.string.update_approval_permission_subtitle) - } else { - resourceReference( - id = R.string.give_permission_swap_subtitle, - formatArgs = wrappedList(providerName, fromSwapCurrencyStatus.currency.symbol), - ) - }, - feeFooter = resourceReference(R.string.swap_give_permission_fee_footer), - isResetApproval = permissionState.isResetApproval, - isHoldToConfirm = isHoldToConfirm, - callback = model.approvalCallback, - ) - } - private fun onChildBack() { val isEmptyStack = childStack.value.backStack.isEmpty() val isSuccess = model.uiState.successState != null diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapProviderStateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapProviderStateBuilder.kt index eccf09da43..f46b835d38 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapProviderStateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapProviderStateBuilder.kt @@ -59,6 +59,7 @@ internal object SwapProviderStateBuilder { ), selectionType = selectionType, percentLowerThenBest = PercentDifference.Empty, + approvalSettings = ProviderState.ApprovalSettings.Empty, onProviderClick = onProviderClick, ) } @@ -76,6 +77,7 @@ internal object SwapProviderStateBuilder { selectionType: ProviderState.SelectionType, needApplyFCARestrictions: Boolean, onProviderClick: (String) -> Unit, + onApprovalSelectClick: (SwapProvider) -> Unit = {}, ): ProviderState.Content { return provider.toContent( subtitle = buildSelectableSubtitle(toTokenInfo), @@ -88,6 +90,12 @@ internal object SwapProviderStateBuilder { percentLowerThenBest = pricesLowerBest[provider.providerId] ?.let(PercentDifference::Value) ?: PercentDifference.Value(0f), + approvalSettings = when (permissionState) { + is PermissionDataState.PermissionSettings -> ProviderState.ApprovalSettings.Content( + onApprovalSelectClick = { onApprovalSelectClick(provider) }, + ) + else -> ProviderState.ApprovalSettings.Empty + }, onProviderClick = onProviderClick, ) } @@ -111,6 +119,7 @@ internal object SwapProviderStateBuilder { ), selectionType = selectionType, percentLowerThenBest = PercentDifference.Empty, + approvalSettings = ProviderState.ApprovalSettings.Empty, onProviderClick = onProviderClick, ) } @@ -147,11 +156,13 @@ internal object SwapProviderStateBuilder { } } + @Suppress("LongParameterList") private fun SwapProvider.toContent( subtitle: TextReference, additionalBadge: ProviderState.AdditionalBadge, selectionType: ProviderState.SelectionType, percentLowerThenBest: PercentDifference, + approvalSettings: ProviderState.ApprovalSettings, onProviderClick: (String) -> Unit, ): ProviderState.Content { return ProviderState.Content( @@ -165,6 +176,7 @@ internal object SwapProviderStateBuilder { percentLowerThenBest = percentLowerThenBest, namePrefix = ProviderState.PrefixType.NONE, onProviderClick = onProviderClick, + approvalSettings = approvalSettings, ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 59973dc978..a95eb71fec 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -15,6 +15,7 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter +import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam @@ -57,6 +58,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.isHotWallet import com.tangem.domain.pay.WithdrawalResult import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase import com.tangem.domain.settings.usercountry.GetUserCountryUseCase @@ -89,10 +91,7 @@ import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.domain.transfer.SwapTransferInteractor -import com.tangem.feature.swap.models.SwapAlertUM -import com.tangem.feature.swap.models.SwapStateHolder -import com.tangem.feature.swap.models.TokenSelectionDirection -import com.tangem.feature.swap.models.UiActions +import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.SwapNotificationUM import com.tangem.feature.swap.router.SwapRoute import com.tangem.feature.swap.ui.StateBuilder @@ -100,6 +99,8 @@ import com.tangem.feature.swap.ui.transfer.SwapTransferStateBuilder import com.tangem.feature.swap.utils.formatToUIRepresentation import com.tangem.feature.swap.utils.getContractAddress import com.tangem.features.approval.api.GiveApprovalComponent +import com.tangem.features.approval.api.GiveApprovalEntryComponent +import com.tangem.features.approval.api.SelectApprovalTypeComponent import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenAnalyticsPayload import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenResult @@ -163,7 +164,7 @@ internal class SwapModel @Inject constructor( private val messageSender: UiMessageSender, private val initialCurrenciesResolver: InitialCurrenciesResolver, private val allowPermissionsHandler: AllowPermissionsHandler, - swapFeatureToggles: SwapFeatureToggles, + private val swapFeatureToggles: SwapFeatureToggles, private val getSwapUiModeUseCase: GetSwapUiModeUseCase, private val setSwapUiModeUseCase: SetSwapUiModeUseCase, private val calculateAmountUseCase: CalculateAmountUseCase, @@ -223,7 +224,7 @@ internal class SwapModel @Inject constructor( } var uiState: SwapStateHolder by mutableStateOf(stateBuilder.createInitialLoadingState()) - private set + internal set val feeSelectorRepository = FeeSelectorRepository() @@ -249,9 +250,17 @@ internal class SwapModel @Inject constructor( private var preselectedFromCurrency: CryptoCurrency? = null private var preselectedToCurrency: CryptoCurrency? = null - val approvalSlotNavigation = SlotNavigation() + val isPermissionNotNeeded: Boolean + get() { + val permissionState = dataState.getCurrentLoadedSwapState()?.permissionState + return permissionState == PermissionDataState.Empty || + swapFeatureToggles.isSwapIntegratedApproveEnabled && + permissionState is PermissionDataState.PermissionSettings + } - val approvalCallback = object : GiveApprovalComponent.Callback { + val approvalSlotNavigation = SlotNavigation() + + internal val approvalFullCallback = object : GiveApprovalComponent.Callback { override fun onApproveClick() {} override fun onApproveDone() { @@ -276,6 +285,46 @@ internal class SwapModel @Inject constructor( } } + internal val approvalSelectorCallback = object : SelectApprovalTypeComponent.Callback { + override fun onApproveTypeSelected(spenderAddress: String, approveType: ApproveType) { + val (swapState, permission) = dataState.lastLoadedSwapStates.firstNotNullOfOrNull { (provider, state) -> + if (state !is SwapState.QuotesLoadedState) return@firstNotNullOfOrNull null + val permissionState = state.permissionState + + if (permissionState is PermissionDataState.PermissionSettings && + permissionState.spenderAddress == spenderAddress + ) { + state to permissionState + } else { + null + } + } ?: return + + if (permission.type == approveType) { + approvalSlotNavigation.dismiss() + return + } + dataState = dataState.copy( + lastLoadedSwapStates = dataState.lastLoadedSwapStates.toMutableMap().apply { + put( + swapState.swapProvider, + swapState.copy(permissionState = permission.copy(type = approveType)), + ) + }, + ) + approvalSlotNavigation.dismiss() + modelScope.launch { + feeSelectorRepository.state.value = FeeSelectorUM.Loading + feeSelectorReloadTrigger.triggerLoadingState() + feeSelectorReloadTrigger.triggerUpdate() + } + } + + override fun onCancelClick() { + approvalSlotNavigation.dismiss() + } + } + init { subscribeToTokenSelection() @@ -963,8 +1012,6 @@ internal class SwapModel @Inject constructor( tokenSwapInfoForProviders = successStates.entries .associate { it.key.providerId to it.value.toTokenInfo }, ) - val isPermissionNotNeeded = - dataState.getCurrentLoadedSwapState()?.permissionState == PermissionDataState.Empty if (shouldUpdateFeeBlock && isPermissionNotNeeded) { modelScope.launch { feeSelectorReloadTrigger.triggerUpdate() } } else { @@ -1751,13 +1798,27 @@ internal class SwapModel @Inject constructor( onPredefinedPercentSelected = ::onPredefinedPercentSelected, onReduceToAmount = ::onReduceAmountClicked, onReduceByAmount = ::onReduceAmountClicked, - openPermissionBottomSheet = { + onApproveClick = { singleTaskScheduler.cancelTask() sendGivePermissionClickedEvent() - approvalSlotNavigation.activate(Unit) + val approval = getApprovalParams() + if (approval != null) { + approvalSlotNavigation.activate( + GiveApprovalEntryComponent.Mode.FullApproval(approval), + ) + } + }, + onApproveTypeSelect = { provider -> + val approval = getSelectApprovalTypeParams(provider) + if (approval != null) { + approvalSlotNavigation.activate( + GiveApprovalEntryComponent.Mode.SelectOnly(approval), + ) + } }, onAmountSelected = { onAmountSelected(it) }, onProviderClick = { providerId -> + singleTaskScheduler.cancelTask() analyticsEventHandler.send(SwapEvents.ProviderClicked()) val states = dataState.lastLoadedSwapStates.getLastLoadedSuccessStates() val pricesLowerBest = getPricesLowerBest(providerId, states) @@ -2359,6 +2420,54 @@ internal class SwapModel @Inject constructor( } } + internal fun getSelectApprovalTypeParams(provider: SwapProvider): SelectApprovalTypeComponent.Params? { + val fromSwapCurrencyStatus = dataState.fromSwapCurrencyStatus ?: return null + val swapState = dataState.lastLoadedSwapStates[provider] as? SwapState.QuotesLoadedState ?: return null + val permissionState = swapState.permissionState as? PermissionDataState.PermissionSettings ?: return null + val providerName = swapState.swapProvider.name + val approvalType = permissionState.type + + return SelectApprovalTypeComponent.Params( + userWalletId = params.userWalletId, + cryptoCurrencyStatus = fromSwapCurrencyStatus.status, + initialApproveType = approvalType, + amountFooter = resourceReference( + id = R.string.give_permission_swap_subtitle, + formatArgs = wrappedList(providerName, fromSwapCurrencyStatus.currency.symbol), + ), + spenderAddress = permissionState.spenderAddress, + callback = approvalSelectorCallback, + ) + } + + internal fun getApprovalParams(): GiveApprovalComponent.Params? { + val permissionState = uiState.permissionUM as? SwapPermissionUM.PermissionRequired ?: return null + val fromSwapCurrencyStatus = dataState.fromSwapCurrencyStatus ?: return null + val feeCryptoCurrency = dataState.feePaidCryptoCurrency ?: return null + val providerName = dataState.selectedProvider?.name.orEmpty() + val isHoldToConfirm = fromSwapCurrencyStatus.userWallet.isHotWallet + + return GiveApprovalComponent.Params( + userWalletId = params.userWalletId, + cryptoCurrencyStatus = fromSwapCurrencyStatus.status, + feeCryptoCurrencyStatus = feeCryptoCurrency, + amount = dataState.amount.orEmpty(), + spenderAddress = permissionState.spenderAddress, + amountFooter = if (permissionState.isResetApproval) { + resourceReference(R.string.update_approval_permission_subtitle) + } else { + resourceReference( + id = R.string.give_permission_swap_subtitle, + formatArgs = wrappedList(providerName, fromSwapCurrencyStatus.currency.symbol), + ) + }, + feeFooter = resourceReference(R.string.swap_give_permission_fee_footer), + isResetApproval = permissionState.isResetApproval, + isHoldToConfirm = isHoldToConfirm, + callback = approvalFullCallback, + ) + } + private companion object { const val INITIAL_AMOUNT = "" const val UPDATE_DELAY = 10000L diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt index 91e28c1b4e..836f97b026 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt @@ -249,7 +249,7 @@ internal class SwapNotificationsFactory( if (quoteModel.permissionState is PermissionDataState.PermissionRequired) { add( SwapNotificationUM.Info.PermissionNeeded( - onApproveClick = actions.openPermissionBottomSheet, + onApproveClick = actions.onApproveClick, onLearnMoreClick = { actions.onLinkClick(TangemSiteUrlBuilder.HELP_CENTER_SWAP_URL) }, ), ) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt index cc76ec32dd..acf53a6f54 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -4,6 +4,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.express.models.ProviderFilterType import com.tangem.domain.swap.models.PredefinedPercentAmount import com.tangem.feature.swap.domain.models.SwapAmount +import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.domain.SwapUIMode import java.math.BigDecimal @@ -18,7 +19,8 @@ internal data class UiActions( val onPredefinedPercentSelected: (PredefinedPercentAmount) -> Unit, val onReduceToAmount: (SwapAmount) -> Unit, val onReduceByAmount: (SwapAmount, reduceBy: BigDecimal) -> Unit, - val openPermissionBottomSheet: () -> Unit, + val onApproveClick: () -> Unit, + val onApproveTypeSelect: (SwapProvider) -> Unit, // region new actions val onRetryClick: () -> Unit, val onProviderClick: (String) -> Unit, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt index 873f90dc83..3ffdf0d429 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -178,6 +178,7 @@ private fun Preview_ChooseProviderBottomSheet() { percentLowerThenBest = PercentDifference.Value(-1.0f), selectionType = ProviderState.SelectionType.SELECT, namePrefix = ProviderState.PrefixType.NONE, + approvalSettings = ProviderState.ApprovalSettings.Empty, onProviderClick = {}, ), ProviderState.Unavailable( diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 7380667baa..69c474027c 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -99,7 +99,7 @@ internal class StateBuilder( onMaxAmountSelected = actions.onMaxAmountSelected, onPredefinedPercentSelected = actions.onPredefinedPercentSelected, changeCardsButtonState = ChangeCardsButtonState.DISABLED, - onShowPermissionBottomSheet = actions.openPermissionBottomSheet, + onShowPermissionBottomSheet = actions.onApproveClick, onSelectTokenClick = actions.onSelectTokenClick, onSuccess = actions.onSuccess, providerState = ProviderState.Empty(), @@ -1033,6 +1033,7 @@ internal class StateBuilder( pricesLowerBest = pricesLowerBest, onProviderSelect = actions.onProviderSelect, needApplyFCARestrictions = needApplyFCARestrictions, + onApprovalSelectClick = actions.onApproveTypeSelect, ) } .sortedWith(ProviderPercentDiffComparator) @@ -1136,6 +1137,7 @@ internal class StateBuilder( private fun Map.Entry.convertToProviderBottomSheetState( pricesLowerBest: Map, onProviderSelect: (String) -> Unit, + onApprovalSelectClick: (SwapProvider) -> Unit, needApplyFCARestrictions: Boolean, ): ProviderState? { val provider = this.key @@ -1150,6 +1152,7 @@ internal class StateBuilder( selectionType = ProviderState.SelectionType.SELECT, needApplyFCARestrictions = needApplyFCARestrictions, onProviderClick = onProviderSelect, + onApprovalSelectClick = onApprovalSelectClick, ) } is SwapState.SwapError -> getProviderStateForError(