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 fd070196c9..f9911c81f8 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 @@ -81,6 +81,12 @@ sealed class AnalyticsParam { override val feeType: FeeType, ) : TxSentFrom("Swap"), TxData + data class Staking( + override val blockchain: String, + override val token: String, + override val feeType: FeeType, + ) : TxSentFrom("Staking"), TxData + data class Approve( override val blockchain: String, override val token: String, diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt index c932b9f1d9..6c93dfccee 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt @@ -25,6 +25,13 @@ sealed class YieldBalance { .filter { it.type == BalanceType.REWARDS } .sumOf { it.amount } } + + fun getValidatorsCount(): Int { + return balance.items + .filterNot { it.validatorAddress.isNullOrBlank() } + .distinctBy { it.validatorAddress } + .size + } } data object Empty : YieldBalance() diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt index 1896d2c250..bea218c179 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt @@ -77,4 +77,9 @@ sealed class TokenScreenAnalyticsEvent( event = "Button - Token Trustline", params = mapOf("Token" to tokenSymbol, "Blockchain" to blockchain), ) + + data class StakingClicked(val token: String) : TokenScreenAnalyticsEvent( + event = "Staking Clicked", + params = mapOf("Token" to token), + ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/StakingAnalyticsEvents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/StakingAnalyticsEvents.kt new file mode 100644 index 0000000000..336486156f --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/StakingAnalyticsEvents.kt @@ -0,0 +1,157 @@ +package com.tangem.features.staking.impl.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.domain.staking.model.stakekit.action.StakingActionType + +internal sealed class StakingAnalyticsEvents( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent( + category = "Staking", + event = event, + params = params, +) { + + data class StakingInfoScreenOpened( + val validatorsCount: Int, + val token: String, + ) : StakingAnalyticsEvents( + event = "Staking Info Screen Opened", + params = mapOf( + "Validators Count" to validatorsCount.toString(), + AnalyticsParam.TOKEN_PARAM to token, + ), + ) + + data class WhatIsStaking(val token: String) : StakingAnalyticsEvents( + event = "Link - What Is Staking", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to token, + ), + ) + + data class AmountScreenOpened(val token: String) : StakingAnalyticsEvents( + event = "Amount Screen Opened", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to token, + ), + ) + + data class ConfirmationScreenOpened( + val token: String, + val validator: String, + val action: StakingActionType, + ) : StakingAnalyticsEvents( + event = "Confirmation Screen Opened", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to token, + "Validator" to validator, + "Action" to action.name, + ), + ) + + data class StakeInProgressScreenOpened( + val validator: String, + val token: String, + ) : StakingAnalyticsEvents( + event = "Stake In Progress Screen Opened", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to token, + "Validator" to validator, + ), + ) + + data class RewardScreenOpened(val token: String) : StakingAnalyticsEvents( + event = "Reward Screen Opened", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to token, + ), + ) + + data class AmountSelectCurrency( + val token: String, + val isAppCurrency: Boolean, + ) : StakingAnalyticsEvents( + event = "Selected Currency", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to token, + AnalyticsParam.TYPE to if (isAppCurrency) "App Currency" else "Token", + ), + ) + + data class ButtonMax(val token: String) : StakingAnalyticsEvents( + event = "Button - Max", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to token, + ), + ) + + data class ButtonCancel( + val source: StakeScreenSource, + val token: String, + ) : StakingAnalyticsEvents( + event = "Button - Cancel", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to token, + AnalyticsParam.SOURCE to source.name, + ), + ) + + data class ButtonValidator( + val source: StakeScreenSource, + val token: String, + ) : StakingAnalyticsEvents( + event = "Button - Validator", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to token, + AnalyticsParam.SOURCE to source.name, + ), + ) + + data class ButtonRewards( + val token: String, + ) : StakingAnalyticsEvents( + event = "Button - Rewards", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to token, + ), + ) + + data class ButtonAction( + val action: String, + val token: String, + val validator: String, + ) : StakingAnalyticsEvents( + event = "Button - $action", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to token, + "Validator" to validator, + ), + ) + + data object ButtonShare : StakingAnalyticsEvents(event = "Button - Share") + + data object ButtonExplore : StakingAnalyticsEvents(event = "Button - Explore") + + data class StakingError( + val token: String, + ) : StakingAnalyticsEvents( + event = "Errors", + params = mapOf(AnalyticsParam.TOKEN_PARAM to token), + ) + + data class TransactionError( + val token: String, + ) : StakingAnalyticsEvents( + event = "Error - Transaction Rejected", + params = mapOf(AnalyticsParam.TOKEN_PARAM to token), + ) +} + +enum class StakeScreenSource { + Info, + Amount, + Confirmation, + Validators, +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt new file mode 100644 index 0000000000..42dc192fbb --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt @@ -0,0 +1,104 @@ +package com.tangem.features.staking.impl.analytics.utils + +import com.tangem.common.ui.bottomsheet.permission.state.ApproveType +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.features.staking.impl.analytics.StakeScreenSource +import com.tangem.features.staking.impl.analytics.StakingAnalyticsEvents +import com.tangem.features.staking.impl.presentation.state.* + +internal class StakingAnalyticSender( + private val analyticsEventHandler: AnalyticsEventHandler, +) { + + fun initialInfoScreen(value: StakingUiState) { + val initialInfoState = value.initialInfoState as? StakingStates.InitialInfoState.Data + val validatorState = initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data + val validatorCount = validatorState?.balance + ?.filterNot { it.validator?.address.isNullOrBlank() } + ?.distinctBy { it.validator?.address } + ?.size ?: 0 + + analyticsEventHandler.send( + StakingAnalyticsEvents.StakingInfoScreenOpened( + validatorsCount = validatorCount, + token = value.cryptoCurrencySymbol, + ), + ) + } + + fun confirmationScreen(value: StakingUiState) { + val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data + val validatorState = confirmationState?.validatorState as? ValidatorState.Content + val validatorName = validatorState?.chosenValidator?.name ?: return + + if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) return + + analyticsEventHandler.send( + StakingAnalyticsEvents.ConfirmationScreenOpened( + token = value.cryptoCurrencySymbol, + validator = validatorName, + action = when (value.actionType) { + StakingActionCommonType.ENTER -> StakingActionType.STAKE + StakingActionCommonType.EXIT -> StakingActionType.UNSTAKE + StakingActionCommonType.PENDING_REWARDS, + StakingActionCommonType.PENDING_OTHER, + -> confirmationState.pendingAction?.type ?: StakingActionType.UNKNOWN + }, + ), + ) + } + + fun screenCancel(value: StakingUiState) { + analyticsEventHandler.send( + StakingAnalyticsEvents.ButtonCancel( + source = when (value.currentStep) { + StakingStep.InitialInfo -> StakeScreenSource.Info + StakingStep.Amount -> StakeScreenSource.Amount + StakingStep.Confirmation -> StakeScreenSource.Confirmation + StakingStep.Validators, + StakingStep.RewardsValidators, + -> StakeScreenSource.Validators + }, + token = value.cryptoCurrencyName, + ), + ) + } + + fun sendTransactionApprovalAnalytics(tokenCryptoCurrency: CryptoCurrency) { + analyticsEventHandler.send( + Basic.TransactionSent( + sentFrom = AnalyticsParam.TxSentFrom.Approve( + blockchain = tokenCryptoCurrency.network.name, + token = tokenCryptoCurrency.symbol, + feeType = AnalyticsParam.FeeType.Normal, + permissionType = ApproveType.LIMITED.name, + ), + memoType = Basic.TransactionSent.MemoType.Null, + ), + ) + } + + fun sendTransactionStakingAnalytics(cryptoCurrency: CryptoCurrency, validator: String) { + analyticsEventHandler.send( + Basic.TransactionSent( + sentFrom = AnalyticsParam.TxSentFrom.Staking( + blockchain = cryptoCurrency.network.name, + token = cryptoCurrency.symbol, + feeType = AnalyticsParam.FeeType.Normal, + ), + memoType = Basic.TransactionSent.MemoType.Null, + ), + ) + analyticsEventHandler.send( + StakingAnalyticsEvents.StakeInProgressScreenOpened( + validator = validator, + token = cryptoCurrency.symbol, + ), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt index cb590bcb4c..1740d65084 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt @@ -54,6 +54,7 @@ internal class StakingFragment : ComposeFragment() { StakingStateRouter( appRouter = appRouter, stateController = stateController, + analyticsEventsHandler = analyticsEventsHandler, ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index 4c450c78a2..0b04c60e11 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -68,6 +68,7 @@ internal class StakingStateController @Inject constructor( clickIntents = StakingClickIntentsStub, walletName = "", cryptoCurrencyName = "", + cryptoCurrencySymbol = "", currentStep = StakingStep.InitialInfo, initialInfoState = StakingStates.InitialInfoState.Empty(), amountState = AmountState.Empty(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index 5dd1f3de27..69cca72194 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -1,14 +1,21 @@ package com.tangem.features.staking.impl.presentation.state import com.tangem.common.routing.AppRouter +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.features.staking.impl.analytics.StakingAnalyticsEvents +import com.tangem.features.staking.impl.analytics.utils.StakingAnalyticSender internal class StakingStateRouter( private val appRouter: AppRouter, private val stateController: StakingStateController, + private val analyticsEventsHandler: AnalyticsEventHandler, ) { + private val analyticSender = StakingAnalyticSender(analyticsEventsHandler) + fun onBackClick() { + analyticSender.screenCancel(stateController.value) appRouter.pop() stateController.clear() } @@ -48,14 +55,21 @@ internal class StakingStateRouter( } private fun showInitial() { + analyticSender.initialInfoScreen(stateController.value) stateController.update { it.copy(currentStep = StakingStep.InitialInfo) } } - fun showRewardsValidators() { + private fun showRewardsValidators() { + analyticsEventsHandler.send( + StakingAnalyticsEvents.RewardScreenOpened(stateController.value.cryptoCurrencySymbol), + ) stateController.update { it.copy(currentStep = StakingStep.RewardsValidators) } } - fun showAmount() { + private fun showAmount() { + analyticsEventsHandler.send( + StakingAnalyticsEvents.AmountScreenOpened(stateController.value.cryptoCurrencySymbol), + ) stateController.update { it.copy(currentStep = StakingStep.Amount) } } @@ -63,7 +77,8 @@ internal class StakingStateRouter( stateController.update { it.copy(currentStep = StakingStep.Validators) } } - fun showConfirmation() { + private fun showConfirmation() { + analyticSender.confirmationScreen(stateController.value) stateController.update { it.copy(currentStep = StakingStep.Confirmation) } } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 1a276f55e0..202219cb3b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -27,6 +27,7 @@ internal data class StakingUiState( val clickIntents: StakingClickIntents, val walletName: String, val cryptoCurrencyName: String, + val cryptoCurrencySymbol: String, val currentStep: StakingStep, val initialInfoState: StakingStates.InitialInfoState, val amountState: AmountState, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index a13675c8bf..4cc05fa7f2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -69,4 +69,6 @@ internal object StakingClickIntentsStub : StakingClickIntents { override fun onNotificationCancel(notification: Class) {} override fun openTokenDetails(cryptoCurrency: CryptoCurrency) {} + + override fun onActiveStakeAnalytic() {} } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 13c748e962..8779bf824e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -56,16 +56,19 @@ internal class SetInitialDataStateTransformer( } private val yieldBalancesConverter by lazy(LazyThreadSafetyMode.NONE) { - YieldBalancesConverter(cryptoCurrencyStatusProvider, appCurrencyProvider, yield) + YieldBalancesConverter( + cryptoCurrencyStatusProvider, + appCurrencyProvider, + yield, + ) } override fun transform(prevState: StakingUiState): StakingUiState { - val cryptoCurrencyName = cryptoCurrencyStatusProvider().currency.name - + val cryptoCurrency = cryptoCurrencyStatusProvider().currency return prevState.copy( title = TextReference.EMPTY, - walletName = userWalletProvider.invoke().name, - cryptoCurrencyName = cryptoCurrencyName, + cryptoCurrencyName = cryptoCurrency.name, + cryptoCurrencySymbol = cryptoCurrency.symbol, clickIntents = clickIntents, currentStep = StakingStep.InitialInfo, initialInfoState = createInitialInfoState(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 6ffdee416f..15aeef6efe 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -173,8 +173,9 @@ private fun LazyListScope.activeStakingBlock( ) { balance -> ActiveStakingBlock( balance = balance, - onClick = clickIntents::onActiveStake, isBalanceHidden = isBalanceHidden, + onClick = clickIntents::onActiveStake, + onAnalytic = clickIntents::onActiveStakeAnalytic, modifier = Modifier .animateItemPlacement() .then( @@ -268,8 +269,9 @@ private fun StakingRewardBlock( @Composable private fun ActiveStakingBlock( balance: BalanceState, - onClick: (BalanceState) -> Unit, isBalanceHidden: Boolean, + onClick: (BalanceState) -> Unit, + onAnalytic: () -> Unit, modifier: Modifier = Modifier, ) { val (icon, iconTint) = balance.type.getIcon() @@ -287,7 +289,10 @@ private fun ActiveStakingBlock( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(), enabled = balance.isClickable, - onClick = { onClick(balance) }, + onClick = { + onAnalytic() + onClick(balance) + }, ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index b9743fb560..ed0cfbffc4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -39,6 +39,8 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onActiveStake(activeStake: BalanceState) + fun onActiveStakeAnalytic() + fun showApprovalBottomSheet() fun onApprovalClick() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index e99d719256..b17622c312 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -15,6 +15,7 @@ import com.tangem.common.routing.bundle.unbundle import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -29,7 +30,9 @@ import com.tangem.domain.staking.* import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType @@ -45,6 +48,9 @@ import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.staking.impl.analytics.StakeScreenSource +import com.tangem.features.staking.impl.analytics.StakingAnalyticsEvents +import com.tangem.features.staking.impl.analytics.utils.StakingAnalyticSender import com.tangem.features.staking.impl.navigation.InnerStakingRouter import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType @@ -106,6 +112,7 @@ internal class StakingViewModel @Inject constructor( private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase, private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, private val isAnyTokenStakedUseCase: IsAnyTokenStakedUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, @DelayedWork private val coroutineScope: CoroutineScope, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -135,6 +142,8 @@ internal class StakingViewModel @Inject constructor( private var userWallet: UserWallet by Delegates.notNull() private var appCurrency: AppCurrency by Delegates.notNull() + private var isInitialInfoAnalyticSent: Boolean = false + private val stakingEventFactory: StakingEventFactory get() = StakingEventFactory( stateController = stateController, @@ -142,6 +151,10 @@ internal class StakingViewModel @Inject constructor( onFailedTxEmailClick = ::onFailedTxEmailClick, ) + private val stakingAnalyticSender = StakingAnalyticSender( + analyticsEventHandler = analyticsEventHandler, + ) + private var stakingApproval: StakingApproval = StakingApproval.Empty private var isAmountSubtractAvailable: Boolean = false private val allowanceTaskScheduler = SingleTaskScheduler() @@ -270,6 +283,7 @@ internal class StakingViewModel @Inject constructor( ), ).getOrElse { Timber.e(it.toString()) + analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) stakingEventFactory.createStakingErrorAlert(it) stateController.update(SetConfirmationStateResetAssentTransformer) return@launch @@ -284,6 +298,7 @@ internal class StakingViewModel @Inject constructor( transactionId = transaction.id, ).getOrElse { Timber.e(it.toString()) + analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) stakingEventFactory.createStakingErrorAlert(it) stateController.update(SetConfirmationStateResetAssentTransformer) return@launch @@ -301,6 +316,7 @@ internal class StakingViewModel @Inject constructor( fullTransactionsData = fullTransactionsData, fee = fee, pendingAction = confirmationState.pendingAction, + validator = validatorState.chosenValidator.name, ) } } @@ -326,6 +342,7 @@ internal class StakingViewModel @Inject constructor( type = pendingAction?.type, ), ).getOrElse { + analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) stateController.update(AddStakingErrorTransformer()) updateNotifications(GetFeeError.UnknownError) return @@ -365,6 +382,7 @@ internal class StakingViewModel @Inject constructor( updateNotifications() }, ifLeft = { + analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencyName)) stateController.update(AddStakingErrorTransformer()) updateNotifications(it) }, @@ -389,6 +407,7 @@ internal class StakingViewModel @Inject constructor( } override fun onInitialInfoBannerClick() { + analyticsEventHandler.send(StakingAnalyticsEvents.WhatIsStaking(cryptoCurrencyStatus.currency.symbol)) innerRouter.openUrl(WHAT_IS_STAKING_ARTICLE_URL) } @@ -409,26 +428,47 @@ internal class StakingViewModel @Inject constructor( } override fun onMaxValueClick() { + analyticsEventHandler.send(StakingAnalyticsEvents.ButtonMax(cryptoCurrencyStatus.currency.symbol)) stateController.update(AmountMaxValueStateTransformer(cryptoCurrencyStatus, yield)) } override fun onCurrencyChangeClick(isFiat: Boolean) { + analyticsEventHandler.send( + StakingAnalyticsEvents.AmountSelectCurrency(cryptoCurrencyStatus.currency.symbol, isFiat), + ) stateController.update(AmountCurrencyChangeStateTransformer(cryptoCurrencyStatus, isFiat)) } - override fun openValidators() = stakingStateRouter.showValidators() + override fun openValidators() { + analyticsEventHandler.send( + StakingAnalyticsEvents.ButtonValidator( + source = StakeScreenSource.Confirmation, + token = cryptoCurrencyStatus.currency.symbol, + ), + ) + stakingStateRouter.showValidators() + } override fun onValidatorSelect(validator: Yield.Validator) { stateController.update(ValidatorSelectChangeTransformer(validator)) } override fun openRewardsValidators() { + analyticsEventHandler.send( + StakingAnalyticsEvents.ButtonRewards(value.cryptoCurrencyName), + ) val rewardsValidators = stateController.value.rewardsValidatorsState as? StakingStates.RewardsValidatorsState.Data val rewards = rewardsValidators?.rewards if (rewards != null && rewards.isSingleItem()) { onActiveStake(rewards.first()) } else { + analyticsEventHandler.send( + StakingAnalyticsEvents.ButtonValidator( + source = StakeScreenSource.Info, + token = cryptoCurrencyStatus.currency.symbol, + ), + ) onNextClick(actionTypeToOverwrite = StakingActionCommonType.PENDING_REWARDS) } } @@ -463,6 +503,15 @@ internal class StakingViewModel @Inject constructor( } } + override fun onActiveStakeAnalytic() { + analyticsEventHandler.send( + StakingAnalyticsEvents.ButtonValidator( + source = StakeScreenSource.Info, + token = cryptoCurrencyStatus.currency.symbol, + ), + ) + } + override fun showApprovalBottomSheet() { stateController.update( ShowApprovalBottomSheetTransformer( @@ -503,6 +552,7 @@ internal class StakingViewModel @Inject constructor( ).fold( ifLeft = { error -> Timber.e(error.toString()) + analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencyName)) stateController.update( SetConfirmationStateAssentApprovalTransformer( appCurrencyProvider = Provider { appCurrency }, @@ -524,6 +574,7 @@ internal class StakingViewModel @Inject constructor( ).fold( ifLeft = { error -> Timber.e(error.toString()) + analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencyName)) stateController.update( SetConfirmationStateAssentApprovalTransformer( appCurrencyProvider = Provider { appCurrency }, @@ -535,6 +586,7 @@ internal class StakingViewModel @Inject constructor( stateController.update(SetConfirmationStateResetAssentTransformer) }, ifRight = { + stakingAnalyticSender.sendTransactionApprovalAnalytics(tokenCryptoCurrency) stateController.update(SetApprovalInProgressTransformer) stateController.update(DismissBottomSheetStateTransformer()) awaitForAllowance(confirmationState.pendingAction) @@ -652,10 +704,10 @@ internal class StakingViewModel @Inject constructor( } override fun onExploreClick() { + analyticsEventHandler.send(StakingAnalyticsEvents.ButtonExplore) val confirmationDataState = uiState.value.confirmationState as? StakingStates.ConfirmationState.Data val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content val txUrl = transactionDoneState?.txUrl - if (txUrl != null) { innerRouter.openUrl(txUrl) } @@ -666,12 +718,11 @@ internal class StakingViewModel @Inject constructor( val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content val txUrl = transactionDoneState?.txUrl + analyticsEventHandler.send(StakingAnalyticsEvents.ButtonShare) if (txUrl != null) { vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click) stakingEventFactory.createShareDialog(txUrl = txUrl) } - - // TODO staking [REDACTED_TASK_KEY] } override fun onFailedTxEmailClick(errorMessage: String) { @@ -742,6 +793,17 @@ internal class StakingViewModel @Inject constructor( .onEach { maybeStatus -> maybeStatus.fold( ifRight = { status -> + if (!isInitialInfoAnalyticSent) { + isInitialInfoAnalyticSent = true + val balances = status.value.yieldBalance as? YieldBalance.Data + analyticsEventHandler.send( + StakingAnalyticsEvents.StakingInfoScreenOpened( + validatorsCount = balances?.getValidatorsCount() ?: 0, + token = status.currency.symbol, + ), + ) + } + feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, status).getOrNull() cryptoCurrencyStatus = status @@ -800,14 +862,33 @@ internal class StakingViewModel @Inject constructor( fullTransactionsData: List, fee: Fee, pendingAction: PendingAction?, + validator: String, ) { + val cryptoCurrency = cryptoCurrencyStatus.currency + val action = when (value.actionType) { + StakingActionCommonType.ENTER -> StakingActionType.STAKE + StakingActionCommonType.EXIT -> StakingActionType.UNSTAKE + StakingActionCommonType.PENDING_REWARDS, + StakingActionCommonType.PENDING_OTHER, + -> pendingAction?.type + } + action?.let { + analyticsEventHandler.send( + StakingAnalyticsEvents.ButtonAction( + action = it.name, + token = value.cryptoCurrencyName, + validator = validator, + ), + ) + } sendMultipleTransactionUseCase( txsData = fullTransactionsData.map { it.tangemTransaction }, userWallet = userWallet, - network = cryptoCurrencyStatus.currency.network, + network = cryptoCurrency.network, ).fold( ifLeft = { error -> Timber.e(error.toString()) + analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) stateController.update( SetConfirmationStateAssentTransformer( appCurrencyProvider = Provider { appCurrency }, @@ -820,6 +901,7 @@ internal class StakingViewModel @Inject constructor( stateController.update(SetConfirmationStateResetAssentTransformer) }, ifRight = { transactionHashes -> + stakingAnalyticSender.sendTransactionStakingAnalytics(cryptoCurrency, validator) transactionsInProgress.clear() submitHash( transactionIds = fullTransactionsData.map { it.stakeKitTransaction.id }, @@ -828,9 +910,8 @@ internal class StakingViewModel @Inject constructor( scheduleUpdates() val txUrl = getExplorerTransactionUrlUseCase( txHash = transactionHashes.last(), - networkId = cryptoCurrencyStatus.currency.network.id, + networkId = cryptoCurrency.network.id, ).getOrElse { "" } - stateController.update( SetConfirmationStateCompletedTransformer( appCurrencyProvider = Provider { appCurrency }, @@ -852,6 +933,7 @@ internal class StakingViewModel @Inject constructor( transactionHash = transactionHash, ) .onLeft { + analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) saveUnsubmittedHashUseCase.invoke( transactionId = transactionId, transactionHash = transactionHash, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 2157443000..5d65adaf1c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -466,6 +466,7 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onStakeBannerClick() { + analyticsEventsHandler.send(TokenScreenAnalyticsEvent.StakingClicked(cryptoCurrency.symbol)) openStaking() }