Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-27 12:16:41 +04:00
parent c62f464b86
commit e047c491cf
11 changed files with 158 additions and 20 deletions

View file

@ -12,11 +12,15 @@ import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.router.slot.dismiss
import com.arkivanov.essenty.lifecycle.subscribe
import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState
import com.tangem.common.ui.swapStoriesScreen.SwapStoriesScreen
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.ui.R
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent
@ -26,6 +30,7 @@ import com.tangem.feature.swap.router.SwapNavScreen
import com.tangem.feature.swap.ui.SwapScreen
import com.tangem.feature.swap.ui.SwapSelectTokenScreen
import com.tangem.feature.swap.ui.SwapSuccessScreen
import com.tangem.features.approval.api.GiveApprovalComponent
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
@ -43,6 +48,7 @@ internal class DefaultSwapComponent @AssistedInject constructor(
private val swapFeeSelectorBlockComponentFactory: SwapFeeSelectorBlockComponent.Factory,
private val sendFeatureToggles: SendFeatureToggles,
private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory,
private val giveApprovalComponentFactory: GiveApprovalComponent.Factory,
) : SwapComponent, AppComponentContext by appComponentContext {
private val model: SwapModel = getOrCreateModel(params)
@ -55,6 +61,21 @@ internal class DefaultSwapComponent @AssistedInject constructor(
childFactory = { configuration, context -> bottomSheetChild(context) },
)
private val approvalSlot = childSlot(
key = APPROVAL_SLOT_KEY,
source = model.approvalSlotNavigation,
serializer = null,
handleBackButton = true,
childFactory = { _, factoryContext ->
val approvalParams = getApprovalParams()
?: error("Approval params are not available")
giveApprovalComponentFactory.create(
context = childByContext(factoryContext),
params = approvalParams,
)
},
)
init {
lifecycle.subscribe(
onStart = model::onStart,
@ -191,6 +212,9 @@ internal class DefaultSwapComponent @AssistedInject constructor(
}
bottomSheet.child?.instance?.BottomSheet()
val approvalSlotState by approvalSlot.subscribeAsState()
approvalSlotState.child?.instance?.BottomSheet()
}
@Suppress("UnsafeCallOnNullableType")
@ -205,6 +229,27 @@ internal class DefaultSwapComponent @AssistedInject constructor(
)
}
fun getApprovalParams(): GiveApprovalComponent.Params? {
val permissionState = model.uiState.permissionState as? GiveTxPermissionState.ReadyForRequest
?: return null
val fromCryptoCurrency = model.dataState.fromCryptoCurrency ?: return null
val feeCryptoCurrency = model.dataState.feePaidCryptoCurrency ?: return null
val providerName = model.dataState.selectedProvider?.name.orEmpty()
return GiveApprovalComponent.Params(
userWalletId = params.userWalletId,
cryptoCurrencyStatus = fromCryptoCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrency,
amount = permissionState.amount,
spenderAddress = requireNotNull(model.dataState.approveDataModel).spenderAddress,
subtitle = resourceReference(
id = R.string.give_permission_swap_subtitle,
formatArgs = wrappedList(providerName, permissionState.currency),
),
callback = model.approvalCallback,
)
}
private fun toBigDecimalOrZero(bigDecimalString: String?): BigDecimal {
return bigDecimalString?.replace(",", ".")?.toBigDecimalOrNull() ?: BigDecimal.ZERO
}
@ -217,5 +262,6 @@ internal class DefaultSwapComponent @AssistedInject constructor(
private companion object {
const val BOTTOM_SHEET_SLOT_KEY = "bottomSheetSlot"
const val FEE_SELECTOR_SLOT_KEY = "feeSelectorSlot"
const val APPROVAL_SLOT_KEY = "approvalSlot"
}
}

View file

@ -14,6 +14,8 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState.InProgress.getApproveTypeOrNull
import com.tangem.features.approval.api.GiveApprovalComponent
import com.tangem.features.approval.api.GiveApprovalFeatureToggles
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -31,9 +33,9 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.toWrappedList
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.utils.InputNumberFormatter
@ -118,6 +120,8 @@ import com.tangem.utils.Provider
import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP
import com.tangem.utils.coroutines.*
import com.tangem.utils.isNullOrZero
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import timber.log.Timber
@ -173,6 +177,7 @@ internal class SwapModel @Inject constructor(
private val appsFlyerStore: AppsFlyerStore,
private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles,
private val messageSender: UiMessageSender,
giveApprovalFeatureToggles: GiveApprovalFeatureToggles,
) : Model() {
private val params = paramsContainer.require<SwapComponent.Params>()
@ -301,6 +306,33 @@ internal class SwapModel @Inject constructor(
get() = swapRouter.currentScreen
val bottomSheetNavigation: SlotNavigation<AddToPortfolioRoute> = SlotNavigation()
val approvalSlotNavigation = SlotNavigation<Unit>()
private val shouldUseGaslessApproval: Boolean = giveApprovalFeatureToggles.isGaslessApprovalEnabled
val approvalCallback = object : GiveApprovalComponent.Callback {
override fun onApproveClick() {
sendPermissionApproveClickedEvent()
}
override fun onApproveDone() {
approvalSlotNavigation.dismiss()
updateWalletBalance()
uiState = stateBuilder.loadingPermissionState(uiState)
startLoadingQuotesFromLastState(isSilent = true)
}
override fun onApproveFailed() {
approvalSlotNavigation.dismiss()
showAlert()
}
override fun onCancelClick() {
approvalSlotNavigation.dismiss()
startLoadingQuotesFromLastState(isSilent = true)
analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked())
}
}
val addToPortfolioCallback = object : AddToPortfolioComponent.Callback {
override fun onDismiss() = bottomSheetNavigation.dismiss()
@ -1224,7 +1256,7 @@ internal class SwapModel @Inject constructor(
fromTokenStatus = fromCryptoCurrency,
approveType = approveType,
txFee = feeForPermission,
spenderAddress = approveDataModel.spenderAddress,
spenderAddress = requireNotNull(dataState.approveDataModel).spenderAddress,
),
)
}.onSuccess { swapTransactionState ->
@ -1785,10 +1817,14 @@ internal class SwapModel @Inject constructor(
openPermissionBottomSheet = {
singleTaskScheduler.cancelTask()
sendGivePermissionClickedEvent()
uiState = stateBuilder.showPermissionBottomSheet(uiState) {
startLoadingQuotesFromLastState(isSilent = true)
analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked())
uiState = stateBuilder.dismissBottomSheet(uiState)
if (shouldUseGaslessApproval) {
approvalSlotNavigation.activate(Unit)
} else {
uiState = stateBuilder.showPermissionBottomSheet(uiState) {
startLoadingQuotesFromLastState(isSilent = true)
analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked())
uiState = stateBuilder.dismissBottomSheet(uiState)
}
}
},
onAmountSelected = { onAmountSelected(it) },

View file

@ -711,6 +711,9 @@ internal class StateBuilder(
fun createSilentLoadState(uiState: SwapStateHolder): SwapStateHolder {
return uiState.copy(
changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS,
notifications = uiState.notifications
.filterNot { it is SwapNotificationUM.Info.PermissionNeeded }
.toImmutableList(),
)
}