From 0e85eb1d78c70868e1da41cdaa9d3f279f9713e1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 13:19:18 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../feature/swap/analytics/SwapEvents.kt | 28 ++- .../tangem/feature/swap/model/SwapModel.kt | 187 ++++++++++++------ 2 files changed, 155 insertions(+), 60 deletions(-) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index 8d8ed4d072..96e04a99e6 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -43,18 +43,31 @@ sealed class SwapEvents( params = mapOf("Send Token" to sendToken, "Receive Token" to receiveToken), ) - data object ButtonGivePermissionClicked : SwapEvents(event = "Button - Give permission") + data class ButtonGivePermissionClicked( + val sendToken: String, + val receiveToken: String, + val provider: SwapProvider, + ) : SwapEvents( + event = "Button - Give permission", + params = mapOf( + "Send Token" to sendToken, + "Receive Token" to receiveToken, + "Provider" to provider.name, + ), + ) data class ButtonPermissionApproveClicked( val sendToken: String, val receiveToken: String, val approveType: ApproveType, + val provider: SwapProvider, ) : SwapEvents( event = "Button - Permission Approve", params = mapOf( "Send Token" to sendToken, "Receive Token" to receiveToken, "Type" to if (approveType == ApproveType.LIMITED) "Current Transaction" else "Unlimited", + "Provider" to provider.name, ), ) @@ -141,4 +154,17 @@ sealed class SwapEvents( data object Recommended : PromoState("Recommended") } } + + data class NoticePermissionNeeded( + val sendToken: String, + val receiveToken: String, + val provider: SwapProvider, + ) : SwapEvents( + event = "Notice - Permission Needed", + params = mapOf( + "Send Token" to sendToken, + "Receive Token" to receiveToken, + "Provider" to provider.name, + ), + ) } \ No newline at end of file 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 d227b21dfd..649b97059f 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 @@ -189,6 +189,7 @@ internal class SwapModel @Inject constructor( private val toTokenBalanceJobHolder = JobHolder() private var isAmountChangedByUser: Boolean = false + private var lastPermissionNotificationTokens: Pair? = null val currentScreen: SwapNavScreen get() = swapRouter.currentScreen @@ -544,60 +545,101 @@ internal class SwapModel @Inject constructor( private fun setupLoadedState(provider: SwapProvider, state: SwapState, fromToken: CryptoCurrencyStatus) { when (state) { is SwapState.QuotesLoadedState -> { - fillLoadedDataState(state, state.permissionState, state.swapDataModel) - val loadedStates = dataState.lastLoadedSwapStates.getLastLoadedSuccessStates() - val bestRatedProviderId = findBestQuoteProvider(loadedStates)?.providerId ?: provider.providerId - uiState = stateBuilder.createQuotesLoadedState( - uiStateHolder = uiState, - quoteModel = state, - fromToken = fromToken.currency, - feeCryptoCurrencyStatus = dataState.feePaidCryptoCurrency, - swapProvider = provider, - bestRatedProviderId = bestRatedProviderId, - isNeedBestRateBadge = dataState.lastLoadedSwapStates.consideredProvidersStates().size > 1, - selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, - isReverseSwapPossible = isReverseSwapPossible(), - needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), - hideFee = tangemPayInput?.isWithdrawal == true, - ) - if (uiState.notifications.any { it is SwapNotificationUM.Error.UnableToCoverFeeWarning }) { - analyticsEventHandler.send( - SwapEvents.NoticeNotEnoughFee( - token = initialCurrencyFrom.symbol, - blockchain = fromToken.currency.network.name, - ), - ) - } + setupQuotesLoadedUiState(provider, state, fromToken) + sendAnalyticsForNotifications(fromToken) + updatePermissionNotificationState(state) } is SwapState.EmptyAmountState -> { - val toTokenStatus = dataState.toCryptoCurrency - uiState = stateBuilder.createQuotesEmptyAmountState( - uiStateHolder = uiState, - emptyAmountState = state, - fromTokenStatus = fromToken, - toTokenStatus = toTokenStatus, - isReverseSwapPossible = isReverseSwapPossible(), - toAccount = dataState.toAccount, - ) + setupEmptyAmountUiState(state, fromToken) + lastPermissionNotificationTokens = null } is SwapState.SwapError -> { - singleTaskScheduler.cancelTask() - uiState = stateBuilder.createQuotesErrorState( - uiStateHolder = uiState, - swapProvider = provider, - fromToken = state.fromTokenInfo, - toToken = dataState.toCryptoCurrency, - expressDataError = state.error, - includeFeeInAmount = state.includeFeeInAmount, - isReverseSwapPossible = isReverseSwapPossible(), - needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), - toAccount = dataState.toAccount, - ) - sendErrorAnalyticsEvent(state.error, provider) + setupErrorUiState(provider, state) + lastPermissionNotificationTokens = null } } } + private fun setupQuotesLoadedUiState( + provider: SwapProvider, + state: SwapState.QuotesLoadedState, + fromToken: CryptoCurrencyStatus, + ) { + fillLoadedDataState(state, state.permissionState, state.swapDataModel) + val loadedStates = dataState.lastLoadedSwapStates.getLastLoadedSuccessStates() + val bestRatedProviderId = findBestQuoteProvider(loadedStates)?.providerId ?: provider.providerId + uiState = stateBuilder.createQuotesLoadedState( + uiStateHolder = uiState, + quoteModel = state, + fromToken = fromToken.currency, + feeCryptoCurrencyStatus = dataState.feePaidCryptoCurrency, + swapProvider = provider, + bestRatedProviderId = bestRatedProviderId, + isNeedBestRateBadge = dataState.lastLoadedSwapStates.consideredProvidersStates().size > 1, + selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, + isReverseSwapPossible = isReverseSwapPossible(), + needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), + hideFee = tangemPayInput?.isWithdrawal == true, + ) + } + + private fun sendAnalyticsForNotifications(fromToken: CryptoCurrencyStatus) { + if (uiState.notifications.any { it is SwapNotificationUM.Error.UnableToCoverFeeWarning }) { + analyticsEventHandler.send( + SwapEvents.NoticeNotEnoughFee( + token = initialCurrencyFrom.symbol, + blockchain = fromToken.currency.network.name, + ), + ) + } + } + + private fun updatePermissionNotificationState(state: SwapState.QuotesLoadedState) { + val fromTokenId = state.fromTokenInfo.cryptoCurrencyStatus + .currency.id.value + val toTokenId = state.toTokenInfo.cryptoCurrencyStatus + .currency.id.value + val currentTokenPair = Pair(fromTokenId, toTokenId) + + when { + uiState.notifications.none { it is SwapNotificationUM.Info.PermissionNeeded } -> { + lastPermissionNotificationTokens = null + } + lastPermissionNotificationTokens != currentTokenPair -> { + sendNoticePermissionNeededEvent() + lastPermissionNotificationTokens = currentTokenPair + } + } + } + + private fun setupEmptyAmountUiState(state: SwapState.EmptyAmountState, fromToken: CryptoCurrencyStatus) { + val toTokenStatus = dataState.toCryptoCurrency + uiState = stateBuilder.createQuotesEmptyAmountState( + uiStateHolder = uiState, + emptyAmountState = state, + fromTokenStatus = fromToken, + toTokenStatus = toTokenStatus, + isReverseSwapPossible = isReverseSwapPossible(), + toAccount = dataState.toAccount, + ) + } + + private fun setupErrorUiState(provider: SwapProvider, state: SwapState.SwapError) { + singleTaskScheduler.cancelTask() + uiState = stateBuilder.createQuotesErrorState( + uiStateHolder = uiState, + swapProvider = provider, + fromToken = state.fromTokenInfo, + toToken = dataState.toCryptoCurrency, + expressDataError = state.error, + includeFeeInAmount = state.includeFeeInAmount, + isReverseSwapPossible = isReverseSwapPossible(), + needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), + toAccount = dataState.toAccount, + ) + sendErrorAnalyticsEvent(state.error, provider) + } + private fun sendErrorAnalyticsEvent(error: ExpressDataError, provider: SwapProvider) { val receiveToken = dataState.toCryptoCurrency?.currency?.let { "${it.network.backendId}:${it.symbol}" @@ -1331,7 +1373,7 @@ internal class SwapModel @Inject constructor( onReduceByAmount = ::onReduceAmountClicked, openPermissionBottomSheet = { singleTaskScheduler.cancelTask() - analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked) + sendGivePermissionClickedEvent() uiState = stateBuilder.showPermissionBottomSheet(uiState) { startLoadingQuotesFromLastState(isSilent = true) analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked) @@ -1579,19 +1621,46 @@ internal class SwapModel @Inject constructor( }.map { it.currency }.contains(chosen.currency) } + private fun sendNoticePermissionNeededEvent() { + val sendTokenSymbol = dataState.fromCryptoCurrency?.currency?.symbol ?: return + val receiveTokenSymbol = dataState.toCryptoCurrency?.currency?.symbol ?: return + val provider = dataState.selectedProvider ?: return + analyticsEventHandler.send( + SwapEvents.NoticePermissionNeeded( + sendToken = sendTokenSymbol, + receiveToken = receiveTokenSymbol, + provider = provider, + ), + ) + } + + private fun sendGivePermissionClickedEvent() { + val sendTokenSymbol = dataState.fromCryptoCurrency?.currency?.symbol ?: return + val receiveTokenSymbol = dataState.toCryptoCurrency?.currency?.symbol ?: return + val provider = dataState.selectedProvider ?: return + analyticsEventHandler.send( + SwapEvents.ButtonGivePermissionClicked( + sendToken = sendTokenSymbol, + receiveToken = receiveTokenSymbol, + provider = provider, + ), + ) + } + private fun sendPermissionApproveClickedEvent() { - val sendTokenSymbol = dataState.fromCryptoCurrency?.currency?.symbol - val receiveTokenSymbol = dataState.toCryptoCurrency?.currency?.symbol - val approveType = uiState.permissionState.getApproveTypeOrNull() - if (sendTokenSymbol != null && receiveTokenSymbol != null && approveType != null) { - analyticsEventHandler.send( - SwapEvents.ButtonPermissionApproveClicked( - sendToken = sendTokenSymbol, - receiveToken = receiveTokenSymbol, - approveType = approveType, - ), - ) - } + val sendTokenSymbol = dataState.fromCryptoCurrency?.currency?.symbol ?: return + val receiveTokenSymbol = dataState.toCryptoCurrency?.currency?.symbol ?: return + val approveType = uiState.permissionState.getApproveTypeOrNull() ?: return + val provider = dataState.selectedProvider ?: return + + analyticsEventHandler.send( + SwapEvents.ButtonPermissionApproveClicked( + sendToken = sendTokenSymbol, + receiveToken = receiveTokenSymbol, + approveType = approveType, + provider = provider, + ), + ) } private fun updateWalletBalance() {