Updated on 2026-08-14
This commit is contained in:
parent
e2d0973392
commit
d9bc010cd0
12 changed files with 242 additions and 94 deletions
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Unit>()
|
||||
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<GiveApprovalEntryComponent.Mode>()
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<SwapProvider, SwapState>.convertToProviderBottomSheetState(
|
||||
pricesLowerBest: Map<String, Float>,
|
||||
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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue