diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 946ad653c4..a670a8226a 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -295,6 +295,7 @@ sealed class AnalyticsParam { const val REFERRAL = "Referral" const val REFERRAL_ID = "Referral_ID" const val SEARCHED = "Searched" + const val RATE_TYPE = "Rate Type" } } diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt index b22f150071..4eee1933a8 100644 --- a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt @@ -1,6 +1,7 @@ package com.tangem.domain.swap.models import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrencyStatus /** @@ -50,4 +51,16 @@ data class SwapCurrenciesGroup( data class SwapCryptoCurrency( val currencyStatus: CryptoCurrencyStatus, val providers: List, -) \ No newline at end of file +) + +/** + * Get initial rate type based on available providers + */ +fun List.getInitialRateType(): ExpressRateType { + val availableRateTypes = this.flatMap { it.rateTypes }.toSet() + return if (availableRateTypes.contains(ExpressRateType.Fixed)) { + ExpressRateType.Fixed + } else { + ExpressRateType.Float + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index 90eea6d0fa..b17f11cbb6 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -53,6 +53,8 @@ import com.tangem.features.swap.v2.impl.chooseprovider.SwapChooseProviderCompone import com.tangem.features.swap.v2.impl.common.SwapAlertFactory import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute +import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents +import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents.NoticeFixedRate.toAnalyticsRateType import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.Debouncer import com.tangem.utils.coroutines.PeriodicTask @@ -117,6 +119,7 @@ internal class SwapAmountModel @Inject constructor( val rateInfoNavigation: SlotNavigation = SlotNavigation() private var isShowBestRateAnimation: Boolean = false + private var lastAmountScreenOpenedCurrencyId: CryptoCurrency.ID? = null private var autoUpdateSubscriberJob: Job? = null @@ -328,6 +331,11 @@ internal class SwapAmountModel @Inject constructor( override fun onRateClick() { val content = uiState.value as? SwapAmountUM.Content ?: return rateInfoNavigation.activate(content.swapRateType) + val event = when (content.swapRateType) { + ExpressRateType.Float -> SendWithSwapAnalyticEvents.NoticeFloatRate + ExpressRateType.Fixed -> SendWithSwapAnalyticEvents.NoticeFixedRate + } + analyticsEventHandler.send(event) } override fun onSeparatorClick() { @@ -593,6 +601,7 @@ internal class SwapAmountModel @Inject constructor( if (currentState != null && currentState.swapRateMode != SwapRateMode.FLOAT_ONLY) { computeAndSetSecondaryAmount(currentState) } + sendAmountScreenOpenedIfNeeded(secondaryStatus) startLoadingQuotesTask(isSilentReload = false) } else { @Suppress("NullableToStringCall") @@ -603,11 +612,33 @@ internal class SwapAmountModel @Inject constructor( | Secondary -> $secondaryStatus """.trimIndent(), ) + analyticsEventHandler.send( + SendWithSwapAnalyticEvents.SendWithSwapError( + errorScreen = SendWithSwapAnalyticEvents.ErrorScreen.Amount, + message = "Invalid cryptocurrencies status: primary=$primaryStatus, secondary=$secondaryStatus", + ), + ) showErrorAlert(errorMessage = null) } } } + private fun sendAmountScreenOpenedIfNeeded(secondaryStatus: CryptoCurrencyStatus) { + val currencyId = secondaryStatus.currency.id + if (lastAmountScreenOpenedCurrencyId == currencyId) return + lastAmountScreenOpenedCurrencyId = currencyId + + val content = uiState.value as? SwapAmountUM.Content ?: return + + analyticsEventHandler.send( + SendWithSwapAnalyticEvents.AmountScreenOpened( + rateType = content.swapRateType.toAnalyticsRateType(), + fromToken = content.primaryCryptoCurrencyStatus.currency, + toToken = secondaryStatus.currency, + ), + ) + } + private suspend fun initCurrencies(primaryStatus: CryptoCurrencyStatus, secondaryStatus: CryptoCurrencyStatus?) { primaryMinimumAmountBoundary = EnterAmountBoundary( amount = getMinimumTransactionAmountSyncUseCase diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt index 2fb41d9e71..f9de0f0a59 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt @@ -11,6 +11,7 @@ import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.swap.models.SwapRateMode +import com.tangem.domain.swap.models.getInitialRateType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountFieldConverter import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM @@ -52,11 +53,7 @@ internal class SwapAmountSecondaryReadyStateTransformer( rateTypes.contains(ExpressRateType.Fixed) -> SwapRateMode.FIXED_ONLY else -> SwapRateMode.FLOAT_ONLY } - val selectedAmountType = if (swapRateMode != SwapRateMode.FLOAT_ONLY) { - ExpressRateType.Fixed - } else { - ExpressRateType.Float - } + val selectedRateType = providers.getInitialRateType() return SwapAmountUM.Content( isPrimaryButtonEnabled = false, primaryAmount = prevState.primaryAmount, @@ -68,13 +65,13 @@ internal class SwapAmountSecondaryReadyStateTransformer( ), secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, swapCurrencies = swapCurrencies, - selectedAmountType = if (selectedAmountType == ExpressRateType.Fixed) { + selectedAmountType = if (selectedRateType == ExpressRateType.Fixed) { SwapAmountType.To } else { SwapAmountType.From }, swapDirection = swapDirection, - swapRateType = selectedAmountType, + swapRateType = selectedRateType, swapQuotes = persistentListOf(), selectedQuote = SwapQuoteUM.Empty, appCurrency = appCurrency, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt index b6d57e6d09..9adf52cf8f 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt @@ -20,11 +20,9 @@ import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.models.account.Account import com.tangem.domain.swap.models.R import com.tangem.domain.swap.models.SwapDirection import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents -import com.tangem.features.send.v2.api.entry.SendEntryRoute import com.tangem.features.send.v2.api.subcomponents.destination.DestinationRoute import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams @@ -34,6 +32,8 @@ import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.common.SwapUtils.SEND_WITH_SWAP_PROVIDER_TYPES import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM +import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents +import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents.NoticeFixedRate.toAnalyticsRateType import com.tangem.features.swap.v2.impl.sendviaswap.confirm.SendWithSwapConfirmComponent import com.tangem.features.swap.v2.impl.sendviaswap.model.SendWithSwapModel import com.tangem.features.swap.v2.impl.sendviaswap.success.SendWithSwapSuccessComponent @@ -87,17 +87,6 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( componentScope.launch { when (val activeComponent = stack.active.instance) { is SwapAmountComponent -> { - if ( - params.currentRoute.value is SendEntryRoute.SendWithSwap && - model.currentRoute.value != stack.active.configuration - ) { - analyticsEventHandler.send( - CommonSendAnalyticEvents.AmountScreenOpened( - categoryName = model.analyticCategoryName, - source = model.analyticsSendSource, - ), - ) - } activeComponent.updateState(model.uiState.value.amountUM) } is SendDestinationComponent -> { @@ -110,25 +99,23 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( activeComponent.updateState(model.uiState.value.destinationUM) } is SendWithSwapConfirmComponent -> { - val fromCurrency = params.currency - val fromDerivationIndex = when (val account = model.accountFlow.value) { - is Account.CryptoPortfolio -> account.derivationIndex.value - is Account.Payment -> TODO("[REDACTED_JIRA]") - null -> null - }.takeIf { model.isAccountModeFlow.value } - analyticsEventHandler.send( - CommonSendAnalyticEvents.ConfirmationScreenOpened( - categoryName = model.analyticCategoryName, - source = model.analyticsSendSource, - sendBlockchain = fromCurrency.network.name, - sendToken = fromCurrency.symbol, - fromDerivationIndex = fromDerivationIndex, - toDerivationIndex = null, - ), - ) if (model.currentRoute.value.isEditMode) { activeComponent.updateState(model.uiState.value) } + val fromCurrency = params.currency + val content = model.uiState.value.amountUM as? SwapAmountUM.Content ?: return@launch + val toCurrency = content.secondaryCryptoCurrencyStatus?.currency ?: return@launch + val rateType = content.swapRateType.toAnalyticsRateType() + val providerName = content.selectedQuote.provider?.name.orEmpty() + + analyticsEventHandler.send( + SendWithSwapAnalyticEvents.ConfirmationScreenOpened( + providerName = providerName, + rateType = rateType, + fromToken = fromCurrency, + toToken = toCurrency, + ), + ) } } model.currentRoute.emit(stack.active.configuration) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt index 24a68d2945..c0d35dcc2d 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt @@ -4,13 +4,16 @@ import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_FROM import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_TO +import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_MESSAGE import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_BLOCKCHAIN import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_TOKEN import com.tangem.core.analytics.models.AnalyticsParam.Key.SEND_BLOCKCHAIN import com.tangem.core.analytics.models.AnalyticsParam.Key.SEND_TOKEN +import com.tangem.core.analytics.models.AnalyticsParam.Key.RATE_TYPE import com.tangem.core.analytics.models.AppsFlyerIncludedEvent +import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents @@ -19,6 +22,40 @@ internal sealed class SendWithSwapAnalyticEvents( params: Map = emptyMap(), ) : AnalyticsEvent(category = CommonSendAnalyticEvents.SEND_CATEGORY, event = event, params = params) { + /** Confirmation screen opened */ + data class ConfirmationScreenOpened( + val providerName: String, + val rateType: RateType, + val fromToken: CryptoCurrency, + val toToken: CryptoCurrency, + ) : SendWithSwapAnalyticEvents( + event = "Send With Swap Confirm Screen Opened", + params = buildMap { + put(SEND_TOKEN, fromToken.symbol) + put(RECEIVE_TOKEN, toToken.symbol) + put(SEND_BLOCKCHAIN, fromToken.network.name) + put(RECEIVE_BLOCKCHAIN, toToken.network.name) + put(RATE_TYPE, rateType.name) + put(PROVIDER, providerName) + }, + ), AppsFlyerIncludedEvent + + /** Amount screen opened */ + data class AmountScreenOpened( + val rateType: RateType, + val fromToken: CryptoCurrency, + val toToken: CryptoCurrency, + ) : SendWithSwapAnalyticEvents( + event = "Send With Swap Amount Screen Opened", + params = buildMap { + put(SEND_TOKEN, fromToken.symbol) + put(RECEIVE_TOKEN, toToken.symbol) + put(SEND_BLOCKCHAIN, fromToken.network.name) + put(RECEIVE_BLOCKCHAIN, toToken.network.name) + put(RATE_TYPE, rateType.name) + }, + ), AppsFlyerIncludedEvent + data class TransactionScreenOpened( val providerName: String, val feeType: AnalyticsParam.FeeType, @@ -72,4 +109,42 @@ internal sealed class SendWithSwapAnalyticEvents( SEND_BLOCKCHAIN to fromToken.network.name, ), ) + + data object NoticeFixedRate : SendWithSwapAnalyticEvents( + event = "Notice - Fixed Rate", + params = emptyMap(), + ) + + data object NoticeFloatRate : SendWithSwapAnalyticEvents( + event = "Notice - Float Rate", + params = emptyMap(), + ) + + data class SendWithSwapError( + val errorScreen: ErrorScreen, + val message: String, + ) : SendWithSwapAnalyticEvents( + event = when (errorScreen) { + ErrorScreen.Amount -> "Send With Swap Amount Screen Error" + ErrorScreen.Confirm -> "Send With Swap Confirm Screen Error" + }, + params = mapOf( + ERROR_MESSAGE to message, + ), + ) + + enum class ErrorScreen { + Amount, + Confirm, + } + + enum class RateType { + Float, + Fixed, + } + + fun ExpressRateType.toAnalyticsRateType(): RateType = when (this) { + ExpressRateType.Float -> RateType.Float + ExpressRateType.Fixed -> RateType.Fixed + } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index 34b87c65bb..2359164b49 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -310,6 +310,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( } } + @Suppress("LongMethod") private fun onSendClick() { val provider = confirmData.quote?.provider ?: return modelScope.launch { @@ -322,6 +323,12 @@ internal class SendWithSwapConfirmModel @Inject constructor( isAmountSubtractAvailable = isAmountSubtractAvailable, onExpressError = { expressError -> uiState.transformerUpdate(SendWithSwapConfirmSendingStateTransformer(false)) + analyticsEventHandler.send( + SendWithSwapAnalyticEvents.SendWithSwapError( + errorScreen = SendWithSwapAnalyticEvents.ErrorScreen.Confirm, + message = "Express error: $expressError", + ), + ) swapAlertFactory.getGenericErrorState( expressError = expressError, onFailedTxEmailClick = { @@ -339,6 +346,12 @@ internal class SendWithSwapConfirmModel @Inject constructor( }, onSendError = { error -> uiState.transformerUpdate(SendWithSwapConfirmSendingStateTransformer(false)) + analyticsEventHandler.send( + SendWithSwapAnalyticEvents.SendWithSwapError( + errorScreen = SendWithSwapAnalyticEvents.ErrorScreen.Confirm, + message = "Send error: ${error?.toString().orEmpty()}", + ), + ) swapAlertFactory.getSendTransactionErrorState( error = error, onFailedTxEmailClick = { _ ->