Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-31 18:26:19 +04:00
parent b517fe39dd
commit bad7b92f65
7 changed files with 154 additions and 37 deletions

View file

@ -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"
}
}

View file

@ -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<ExpressProvider>,
)
)
/**
* Get initial rate type based on available providers
*/
fun List<ExpressProvider>.getInitialRateType(): ExpressRateType {
val availableRateTypes = this.flatMap { it.rateTypes }.toSet()
return if (availableRateTypes.contains(ExpressRateType.Fixed)) {
ExpressRateType.Fixed
} else {
ExpressRateType.Float
}
}

View file

@ -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<ExpressRateType> = 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

View file

@ -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,

View file

@ -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)

View file

@ -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<String, String> = 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
}
}

View file

@ -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 = { _ ->