Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-25 19:50:53 +05:00
parent 3d16de3f4f
commit ed25735b31
9 changed files with 184 additions and 94 deletions

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.v2.send.confirm.model
package com.tangem.features.send.v2.common
import com.tangem.common.ui.alerts.TransactionErrorAlertConverter
import com.tangem.common.ui.alerts.models.AlertDemoModeUM

View file

@ -0,0 +1,109 @@
package com.tangem.features.send.v2.common.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
/**
* Send screen analytics
*/
internal sealed class CommonSendAnalyticEvents(
category: String,
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = category, event = event, params = params) {
/** Recipient address screen opened */
data class AddressScreenOpened(
val categoryName: String,
) : CommonSendAnalyticEvents(category = categoryName, event = "Address Screen Opened")
/** Amount screen opened */
data class AmountScreenOpened(
val categoryName: String,
) : CommonSendAnalyticEvents(category = categoryName, event = "Amount Screen Opened")
/** Fee screen opened */
data class FeeScreenOpened(
val categoryName: String,
) : CommonSendAnalyticEvents(category = categoryName, event = "Fee Screen Opened")
/** Confirmation screen opened */
data class ConfirmationScreenOpened(
val categoryName: String,
) : CommonSendAnalyticEvents(category = categoryName, event = "Confirm Screen Opened")
/** If transaction delays notification is present */
data class NoticeTransactionDelays(
val categoryName: String,
val token: String,
) : CommonSendAnalyticEvents(
category = categoryName,
event = "Notice - Transaction Delays Are Possible",
params = mapOf(TOKEN_PARAM to token),
)
/** If error occurs during send transactions */
data class TransactionError(
val categoryName: String,
val token: String,
) : CommonSendAnalyticEvents(
category = categoryName,
event = "Error - Transaction Rejected",
params = mapOf(TOKEN_PARAM to token),
)
/** Close button clicked */
data class CloseButtonClicked(
val categoryName: String,
val source: SendScreenSource,
val isFromSummary: Boolean,
val isValid: Boolean,
) : CommonSendAnalyticEvents(
category = categoryName,
event = "Button - Close",
params = mapOf(
SOURCE to source.name,
"FromSummary" to if (isFromSummary) "Yes" else "No",
"isValid" to if (isValid) "Yes" else "No",
),
)
/** Share button clicked */
data class ShareButtonClicked(
val categoryName: String,
) : CommonSendAnalyticEvents(
category = categoryName,
event = "Button - Share",
)
/** Expore button clicked */
data class ExploreButtonClicked(
val categoryName: String,
) : CommonSendAnalyticEvents(
category = categoryName,
event = "Button - Explore",
)
/** Screen reopened from confirmation screen */
data class ScreenReopened(
val categoryName: String,
val source: SendScreenSource,
) : CommonSendAnalyticEvents(
category = categoryName,
event = "Screen Reopened",
params = mapOf(SOURCE to source.name),
)
companion object {
const val SEND_CATEGORY = "Token / Send"
const val NFT_SEND_CATEGORY = "NFT"
}
internal enum class SendScreenSource {
Address,
Amount,
Fee,
Confirm,
}
}

View file

@ -18,13 +18,15 @@ 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.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.send.v2.api.SendComponent
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.common.PredefinedValues
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource
import com.tangem.features.send.v2.common.ui.SendContent
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents.SendScreenSource
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.send.confirm.SendConfirmComponent
import com.tangem.features.send.v2.send.model.SendModel
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent
@ -48,6 +50,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
) : SendComponent, AppComponentContext by appComponentContext {
private val stackNavigation = StackNavigation<CommonSendRoute>()
private val analyticCategoryName = CommonSendAnalyticEvents.SEND_CATEGORY
private val innerRouter = InnerRouter<CommonSendRoute>(
stackNavigation = stackNavigation,
@ -88,19 +91,27 @@ internal class DefaultSendComponent @AssistedInject constructor(
componentScope.launch {
when (val activeComponent = stack.active.instance) {
is SendConfirmComponent -> if (currentRoute.value.isEditMode) {
analyticsEventHandler.send(SendAnalyticEvents.ConfirmationScreenOpened)
analyticsEventHandler.send(
CommonSendAnalyticEvents.ConfirmationScreenOpened(categoryName = analyticCategoryName),
)
activeComponent.updateState(model.uiState.value)
}
is SendAmountComponent -> {
analyticsEventHandler.send(SendAnalyticEvents.AmountScreenOpened)
analyticsEventHandler.send(
CommonSendAnalyticEvents.AmountScreenOpened(categoryName = analyticCategoryName),
)
activeComponent.updateState(model.uiState.value.amountUM)
}
is SendDestinationComponent -> {
analyticsEventHandler.send(SendAnalyticEvents.AddressScreenOpened)
analyticsEventHandler.send(
CommonSendAnalyticEvents.AddressScreenOpened(categoryName = analyticCategoryName),
)
activeComponent.updateState(model.uiState.value.destinationUM)
}
is SendFeeComponent -> {
analyticsEventHandler.send(SendAnalyticEvents.FeeScreenOpened)
analyticsEventHandler.send(
CommonSendAnalyticEvents.FeeScreenOpened(categoryName = analyticCategoryName),
)
}
}
currentRoute.emit(stack.active.configuration)
@ -135,7 +146,8 @@ internal class DefaultSendComponent @AssistedInject constructor(
state = model.uiState.value.destinationUM,
currentRoute = currentRoute.filterIsInstance<CommonSendRoute.Destination>(),
isBalanceHidingFlow = model.isBalanceHiddenFlow,
analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY,
analyticsCategoryName = analyticCategoryName,
title = resourceReference(R.string.send_recipient_label),
userWalletId = params.userWalletId,
cryptoCurrency = params.currency,
callback = model,
@ -156,7 +168,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
state = model.uiState.value.amountUM,
currentRoute = currentRoute.filterIsInstance<CommonSendRoute.Amount>(),
isBalanceHidingFlow = model.isBalanceHiddenFlow,
analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY,
analyticsCategoryName = analyticCategoryName,
userWallet = model.userWallet,
appCurrency = model.appCurrency,
cryptoCurrencyStatus = model.cryptoCurrencyStatus,
@ -167,7 +179,8 @@ internal class DefaultSendComponent @AssistedInject constructor(
onChildBack()
} else {
analyticsEventHandler.send(
SendAnalyticEvents.CloseButtonClicked(
CommonSendAnalyticEvents.CloseButtonClicked(
categoryName = analyticCategoryName,
source = SendScreenSource.Amount,
isFromSummary = false,
isValid = model.uiState.value.amountUM.isPrimaryButtonEnabled,
@ -196,7 +209,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
params = SendFeeComponentParams.FeeParams(
state = model.uiState.value.feeUM,
currentRoute = currentRoute.filterIsInstance<CommonSendRoute.Fee>(),
analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY,
analyticsCategoryName = analyticCategoryName,
userWallet = model.userWallet,
cryptoCurrencyStatus = model.cryptoCurrencyStatus,
feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus,
@ -235,7 +248,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
userWallet = model.userWallet,
currentRoute = currentRoute,
isBalanceHidingFlow = model.isBalanceHiddenFlow,
analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY,
analyticsCategoryName = analyticCategoryName,
cryptoCurrencyStatus = model.cryptoCurrencyStatus,
feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus,
appCurrency = model.appCurrency,

View file

@ -3,8 +3,8 @@ package com.tangem.features.send.v2.send.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents
/**
* Send screen analytics
@ -12,19 +12,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
internal sealed class SendAnalyticEvents(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = SEND_CATEGORY, event = event, params = params) {
/** Recipient address screen opened */
data object AddressScreenOpened : SendAnalyticEvents(event = "Address Screen Opened")
/** Amount screen opened */
data object AmountScreenOpened : SendAnalyticEvents(event = "Amount Screen Opened")
/** Fee screen opened */
data object FeeScreenOpened : SendAnalyticEvents(event = "Fee Screen Opened")
/** Confirmation screen opened */
data object ConfirmationScreenOpened : SendAnalyticEvents(event = "Confirm Screen Opened")
) : AnalyticsEvent(category = CommonSendAnalyticEvents.SEND_CATEGORY, event = event, params = params) {
/** Transaction send screen opened */
data class TransactionScreenOpened(
@ -37,55 +25,4 @@ internal sealed class SendAnalyticEvents(
FEE_TYPE to feeType.value,
),
)
/** If transaction delays notification is present */
data class NoticeTransactionDelays(
val token: String,
) : SendAnalyticEvents(
event = "Notice - Transaction Delays Are Possible",
params = mapOf(TOKEN_PARAM to token),
)
/** If error occurs during send transactions */
data class TransactionError(val token: String) : SendAnalyticEvents(
event = "Error - Transaction Rejected",
params = mapOf(TOKEN_PARAM to token),
)
/** Close button clicked */
data class CloseButtonClicked(
val source: SendScreenSource,
val isFromSummary: Boolean,
val isValid: Boolean,
) : SendAnalyticEvents(
event = "Button - Close",
params = mapOf(
SOURCE to source.name,
"FromSummary" to if (isFromSummary) "Yes" else "No",
"isValid" to if (isValid) "Yes" else "No",
),
)
/** Share button clicked */
data object ShareButtonClicked : SendAnalyticEvents(event = "Button - Share")
/** Expore button clicked */
data object ExploreButtonClicked : SendAnalyticEvents(event = "Button - Explore")
/** Screen reopened from confirmation screen */
data class ScreenReopened(val source: SendScreenSource) : SendAnalyticEvents(
event = "Screen Reopened",
params = mapOf(SOURCE to source.name),
)
companion object {
const val SEND_CATEGORY = "Token / Send"
}
internal enum class SendScreenSource {
Address,
Amount,
Fee,
Confirm,
}
}

View file

@ -32,11 +32,12 @@ import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.common.SendBalanceUpdater
import com.tangem.features.send.v2.common.SendConfirmAlertFactory
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.ui.state.NavigationUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents.SendScreenSource
import com.tangem.features.send.v2.send.analytics.SendAnalyticHelper
import com.tangem.features.send.v2.send.confirm.SendConfirmComponent
import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmInitialStateTransformer
@ -93,6 +94,7 @@ internal class SendConfirmModel @Inject constructor(
private val params: SendConfirmComponent.Params = paramsContainer.require()
private val analyticsCategoryName = params.analyticsCategoryName
private val userWallet = params.userWallet
private val appCurrency = params.appCurrency
private val cryptoCurrencyStatus = params.cryptoCurrencyStatus
@ -165,7 +167,12 @@ internal class SendConfirmModel @Inject constructor(
val confirmUM = it.confirmUM as? ConfirmUM.Content
it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM)
}
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address))
analyticsEventHandler.send(
CommonSendAnalyticEvents.ScreenReopened(
categoryName = analyticsCategoryName,
source = SendScreenSource.Address,
),
)
router.push(CommonSendRoute.Destination(isEditMode = true))
}
}
@ -177,7 +184,12 @@ internal class SendConfirmModel @Inject constructor(
val confirmUM = it.confirmUM as? ConfirmUM.Content
it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM)
}
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount))
analyticsEventHandler.send(
CommonSendAnalyticEvents.ScreenReopened(
categoryName = analyticsCategoryName,
source = SendScreenSource.Amount,
),
)
router.push(CommonSendRoute.Amount(isEditMode = true))
}
}
@ -189,7 +201,12 @@ internal class SendConfirmModel @Inject constructor(
val confirmUM = it.confirmUM as? ConfirmUM.Content
it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM)
}
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee))
analyticsEventHandler.send(
CommonSendAnalyticEvents.ScreenReopened(
categoryName = analyticsCategoryName,
source = SendScreenSource.Fee,
),
)
router.push(CommonSendRoute.Fee)
}
}
@ -207,13 +224,13 @@ internal class SendConfirmModel @Inject constructor(
override fun onExploreClick() {
val confirmUM = uiState.value.confirmUM as? ConfirmUM.Success ?: return
analyticsEventHandler.send(SendAnalyticEvents.ExploreButtonClicked)
analyticsEventHandler.send(CommonSendAnalyticEvents.ExploreButtonClicked(analyticsCategoryName))
urlOpener.openUrl(confirmUM.txUrl)
}
override fun onShareClick() {
val confirmUM = uiState.value.confirmUM as? ConfirmUM.Success ?: return
analyticsEventHandler.send(SendAnalyticEvents.ShareButtonClicked)
analyticsEventHandler.send(CommonSendAnalyticEvents.ShareButtonClicked(analyticsCategoryName))
shareManager.shareText(confirmUM.txUrl)
}
@ -347,7 +364,12 @@ internal class SendConfirmModel @Inject constructor(
popBack = appRouter::pop,
onFailedTxEmailClick = ::onFailedTxEmailClick,
)
analyticsEventHandler.send(SendAnalyticEvents.TransactionError(cryptoCurrency.symbol))
analyticsEventHandler.send(
CommonSendAnalyticEvents.TransactionError(
categoryName = analyticsCategoryName,
token = cryptoCurrency.symbol,
),
)
},
ifRight = {
updateTransactionStatus(txData)
@ -419,6 +441,7 @@ internal class SendConfirmModel @Inject constructor(
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrencyStatus.currency,
appCurrency = appCurrency,
analyticsCategoryName = params.analyticsCategoryName,
).transform(uiState.value.confirmUM),
)
}
@ -445,7 +468,8 @@ internal class SendConfirmModel @Inject constructor(
backIconRes = R.drawable.ic_close_24,
backIconClick = {
analyticsEventHandler.send(
SendAnalyticEvents.CloseButtonClicked(
CommonSendAnalyticEvents.CloseButtonClicked(
categoryName = analyticsCategoryName,
source = SendScreenSource.Confirm,
isFromSummary = true,
isValid = confirmUM.isPrimaryButtonEnabled,

View file

@ -14,9 +14,9 @@ import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.fee.model.checkIfFeeTooHigh
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
@ -30,6 +30,7 @@ internal class SendConfirmationNotificationsTransformer(
private val analyticsEventHandler: AnalyticsEventHandler,
private val cryptoCurrency: CryptoCurrency,
private val appCurrency: AppCurrency,
private val analyticsCategoryName: String,
) : Transformer<ConfirmUM> {
override fun transform(prevState: ConfirmUM): ConfirmUM {
val state = prevState as? ConfirmUM.Content ?: return prevState
@ -52,7 +53,10 @@ internal class SendConfirmationNotificationsTransformer(
if (feeSelectorUM.selectedType == FeeType.Custom && minimumValue > customValue) {
add(NotificationUM.Warning.FeeTooLow)
analyticsEventHandler.send(
SendAnalyticEvents.NoticeTransactionDelays(cryptoCurrency.symbol),
CommonSendAnalyticEvents.NoticeTransactionDelays(
categoryName = analyticsCategoryName,
token = cryptoCurrency.symbol,
),
)
}
}

View file

@ -37,7 +37,7 @@ import com.tangem.features.send.v2.common.PredefinedValues
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.ui.state.NavigationUM
import com.tangem.features.send.v2.send.confirm.SendConfirmComponent
import com.tangem.features.send.v2.send.confirm.model.SendConfirmAlertFactory
import com.tangem.features.send.v2.common.SendConfirmAlertFactory
import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent
import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateQRTrigger

View file

@ -1,5 +1,6 @@
package com.tangem.features.send.v2.subcomponents.destination
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.send.v2.common.CommonSendRoute
@ -21,6 +22,7 @@ internal sealed class SendDestinationComponentParams {
override val analyticsCategoryName: String,
override val cryptoCurrency: CryptoCurrency,
override val userWalletId: UserWalletId,
val title: TextReference,
val isBalanceHidingFlow: StateFlow<Boolean>,
val currentRoute: Flow<CommonSendRoute.Destination>,
val callback: ModelCallback,

View file

@ -22,10 +22,10 @@ import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.send.v2.common.PredefinedValues
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource
import com.tangem.features.send.v2.common.ui.state.NavigationUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents.SendScreenSource
import com.tangem.features.send.v2.send.ui.state.ButtonsUM
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams
@ -289,7 +289,7 @@ internal class SendDestinationModel @Inject constructor(
).onEach { (state, route) ->
params.callback.onNavigationResult(
NavigationUM.Content(
title = resourceReference(R.string.send_recipient_label),
title = params.title,
subtitle = null,
backIconRes = if (route.isEditMode) {
R.drawable.ic_back_24
@ -299,7 +299,8 @@ internal class SendDestinationModel @Inject constructor(
backIconClick = {
if (!route.isEditMode) {
analyticsEventHandler.send(
SendAnalyticEvents.CloseButtonClicked(
CommonSendAnalyticEvents.CloseButtonClicked(
categoryName = params.analyticsCategoryName,
source = SendScreenSource.Address,
isFromSummary = false,
isValid = state.isPrimaryButtonEnabled,