Updated on 2026-08-14

This commit is contained in:
Tangem 2024-10-28 11:28:21 +02:00
parent a08f570a34
commit 72ea156756
18 changed files with 276 additions and 590 deletions

View file

@ -1,179 +0,0 @@
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.StakingError
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
internal sealed class StakingAnalyticsEvents(
event: String,
params: Map<String, String> = 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.asAnalyticName,
),
)
data class StakeInProgressScreenOpened(
val validator: String,
val token: String,
val action: StakingActionType,
) : StakingAnalyticsEvents(
event = "Stake In Progress Screen Opened",
params = mapOf(
AnalyticsParam.TOKEN_PARAM to token,
"Validator" to validator,
"Action" to action.asAnalyticName,
),
)
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 ValidatorChosen(
val token: String,
val validator: String,
) : StakingAnalyticsEvents(
event = "Validator Chosen",
params = mapOf(
AnalyticsParam.TOKEN_PARAM to token,
"Validator" to validator,
),
)
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: StakingActionType,
val token: String,
val validator: String,
) : StakingAnalyticsEvents(
event = "Button - ${action.asAnalyticName}",
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 StakekitError(
val token: String,
val stakeKitError: StakingError,
) : StakingAnalyticsEvents(
event = "Errors",
params = mapOf(
AnalyticsParam.TOKEN_PARAM to token,
if (stakeKitError is StakingError.UnknownError) {
AnalyticsParam.ERROR_DESCRIPTION to (stakeKitError.message ?: "Unknown")
} else {
AnalyticsParam.ERROR_TYPE to stakeKitError.javaClass.simpleName
},
),
)
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,
}

View file

@ -0,0 +1,23 @@
package com.tangem.features.staking.impl.analytics
import com.tangem.core.analytics.api.ParamsInterceptor
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent
class StakingParamsInterceptor(private val tokenSymbol: String) : ParamsInterceptor {
override fun id() = ID
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean {
return event is StakingAnalyticsEvent
}
override fun intercept(params: MutableMap<String, String>) {
params[AnalyticsParam.TOKEN_PARAM] = tokenSymbol
}
companion object {
const val ID = "StakingParamsInterceptorId"
}
}

View file

@ -7,8 +7,8 @@ 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.domain.staking.analytics.StakeScreenSource
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent
import com.tangem.features.staking.impl.presentation.state.*
internal class StakingAnalyticSender(
@ -24,9 +24,8 @@ internal class StakingAnalyticSender(
?.size ?: 0
analyticsEventHandler.send(
StakingAnalyticsEvents.StakingInfoScreenOpened(
StakingAnalyticsEvent.StakingInfoScreenOpened(
validatorsCount = validatorCount,
token = value.cryptoCurrencySymbol,
),
)
}
@ -39,8 +38,7 @@ internal class StakingAnalyticSender(
if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) return
analyticsEventHandler.send(
StakingAnalyticsEvents.ConfirmationScreenOpened(
token = value.cryptoCurrencySymbol,
StakingAnalyticsEvent.ConfirmationScreenOpened(
validator = validatorName,
action = getStakingActionType(value),
),
@ -49,7 +47,7 @@ internal class StakingAnalyticSender(
fun screenCancel(value: StakingUiState) {
analyticsEventHandler.send(
StakingAnalyticsEvents.ButtonCancel(
StakingAnalyticsEvent.ButtonCancel(
source = when (value.currentStep) {
StakingStep.InitialInfo -> StakeScreenSource.Info
StakingStep.Amount -> StakeScreenSource.Amount
@ -58,7 +56,6 @@ internal class StakingAnalyticSender(
StakingStep.RewardsValidators,
-> StakeScreenSource.Validators
},
token = value.cryptoCurrencyName,
),
)
}
@ -93,9 +90,8 @@ internal class StakingAnalyticSender(
),
)
analyticsEventHandler.send(
StakingAnalyticsEvents.StakeInProgressScreenOpened(
StakingAnalyticsEvent.StakeInProgressScreenOpened(
validator = validatorName,
token = value.cryptoCurrencySymbol,
action = getStakingActionType(value),
),
)
@ -107,9 +103,8 @@ internal class StakingAnalyticSender(
val validatorName = validatorState?.chosenValidator?.name ?: return
analyticsEventHandler.send(
StakingAnalyticsEvents.ButtonAction(
StakingAnalyticsEvent.ButtonAction(
action = getStakingActionType(value),
token = value.cryptoCurrencySymbol,
validator = validatorName,
),
)

View file

@ -3,7 +3,7 @@ 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.domain.staking.analytics.StakingAnalyticsEvent
import com.tangem.features.staking.impl.analytics.utils.StakingAnalyticSender
internal class StakingStateRouter(
@ -60,16 +60,12 @@ internal class StakingStateRouter(
}
private fun showRewardsValidators() {
analyticsEventsHandler.send(
StakingAnalyticsEvents.RewardScreenOpened(stateController.value.cryptoCurrencySymbol),
)
analyticsEventsHandler.send(StakingAnalyticsEvent.RewardScreenOpened)
stateController.update { it.copy(currentStep = StakingStep.RewardsValidators) }
}
private fun showAmount() {
analyticsEventsHandler.send(
StakingAnalyticsEvents.AmountScreenOpened(stateController.value.cryptoCurrencySymbol),
)
analyticsEventsHandler.send(StakingAnalyticsEvent.AmountScreenOpened)
stateController.update { it.copy(currentStep = StakingStep.Amount) }
}

View file

@ -135,7 +135,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
}
if (result.isNullOrEmpty()) {
onStakingFeeError(StakingError.UnknownError())
onStakingFeeError(StakingError.DomainError("Error estimating fee"))
return
}

View file

@ -4,7 +4,6 @@ import arrow.core.getOrElse
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.staking.*
import com.tangem.domain.staking.model.PendingTransaction
import com.tangem.domain.staking.model.SubmitHashData
@ -20,7 +19,6 @@ import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.staking.impl.analytics.StakingAnalyticsEvents
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount
import com.tangem.features.staking.impl.presentation.state.utils.isSolanaWithdraw
@ -46,7 +44,6 @@ internal class StakingTransactionSender @AssistedInject constructor(
private val submitHashUseCase: SubmitHashUseCase,
private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase,
private val savePendingTransactionUseCase: SavePendingTransactionUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
@Assisted private val userWallet: UserWallet,
@Assisted private val yield: Yield,
@ -84,7 +81,7 @@ internal class StakingTransactionSender @AssistedInject constructor(
)
if (fullTransactionsData.isNullOrEmpty()) {
onConstructError(StakingError.UnknownError())
onConstructError(StakingError.DomainError("fullTransactionsData is null or empty"))
return
}
@ -198,12 +195,6 @@ internal class StakingTransactionSender @AssistedInject constructor(
type = action?.type,
),
).getOrElse {
analyticsEventHandler.send(
StakingAnalyticsEvents.StakekitError(
token = state.cryptoCurrencySymbol,
stakeKitError = it,
),
)
onConstructError(it)
return emptyList()
}
@ -255,13 +246,7 @@ internal class StakingTransactionSender @AssistedInject constructor(
pendingTransaction = pendingTransaction,
),
)
.onLeft { error ->
analyticsEventHandler.send(
StakingAnalyticsEvents.StakekitError(
token = stateController.value.cryptoCurrencySymbol,
stakeKitError = error,
),
)
.onLeft {
saveUnsubmittedHashUseCase.invoke(
transactionId = transaction.id,
transactionHash = transactionHash,

View file

@ -32,14 +32,8 @@ internal class AddStakingErrorTransformer(
}
private fun convertToNotification(error: StakingError): NotificationUM {
return when (error) {
is StakingError.StakedPositionNotFoundError -> StakingNotification.Error.StakedPositionNotFoundError(
message = error.toString(),
)
// TODO staking
else -> StakingNotification.Error.Common(
subtitle = stringReference(error.toString()),
)
}
return StakingNotification.Error.Common(
subtitle = stringReference(error.toString()),
)
}
}

View file

@ -15,6 +15,7 @@ import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.api.ParamsInterceptorHolder
import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
@ -47,8 +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.domain.staking.analytics.StakeScreenSource
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent
import com.tangem.features.staking.impl.analytics.StakingParamsInterceptor
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.*
@ -106,6 +108,7 @@ internal class StakingViewModel @Inject constructor(
private val stakingBalanceUpdater: StakingBalanceUpdater.Factory,
private val analyticsEventHandler: AnalyticsEventHandler,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val paramsInterceptorHolder: ParamsInterceptorHolder,
@DelayedWork private val coroutineScope: CoroutineScope,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents {
@ -201,6 +204,7 @@ internal class StakingViewModel @Inject constructor(
override fun onCleared() {
super.onCleared()
paramsInterceptorHolder.removeParamsInterceptor(StakingParamsInterceptor.ID)
approvalJobHolder.cancel()
feeJobHolder.cancel()
sendTransactionJobHolder.cancel()
@ -297,18 +301,12 @@ internal class StakingViewModel @Inject constructor(
)
updateNotifications()
},
onStakingFeeError = { error ->
analyticsEventHandler.send(
StakingAnalyticsEvents.StakekitError(
value.cryptoCurrencySymbol,
error,
),
)
onStakingFeeError = {
stateController.update(AddStakingErrorTransformer())
updateNotifications(GetFeeError.UnknownError)
},
onFeeError = { error ->
analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencySymbol))
analyticsEventHandler.send(StakingAnalyticsEvent.TransactionError)
stateController.update(AddStakingErrorTransformer())
updateNotifications(error)
},
@ -336,13 +334,6 @@ internal class StakingViewModel @Inject constructor(
transactionsInProgress.addAll(constructedTransactions)
},
onConstructError = { error ->
Timber.e(error.toString())
analyticsEventHandler.send(
StakingAnalyticsEvents.StakekitError(
token = value.cryptoCurrencySymbol,
stakeKitError = error,
),
)
stakingEventFactory.createStakingErrorAlert(error)
stateController.update(SetConfirmationStateResetAssentTransformer)
},
@ -353,7 +344,7 @@ internal class StakingViewModel @Inject constructor(
},
onSendError = { error ->
Timber.e(error.toString())
analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencySymbol))
analyticsEventHandler.send(StakingAnalyticsEvent.TransactionError)
stakingEventFactory.createSendTransactionErrorAlert(error)
stateController.update(SetConfirmationStateResetAssentTransformer)
},
@ -399,7 +390,7 @@ internal class StakingViewModel @Inject constructor(
}
override fun onInitialInfoBannerClick() {
analyticsEventHandler.send(StakingAnalyticsEvents.WhatIsStaking(value.cryptoCurrencySymbol))
analyticsEventHandler.send(StakingAnalyticsEvent.WhatIsStaking)
innerRouter.openUrl(WHAT_IS_STAKING_ARTICLE_URL)
}
@ -446,22 +437,21 @@ internal class StakingViewModel @Inject constructor(
}
override fun onMaxValueClick() {
analyticsEventHandler.send(StakingAnalyticsEvents.ButtonMax(value.cryptoCurrencySymbol))
analyticsEventHandler.send(StakingAnalyticsEvent.ButtonMax)
stateController.update(AmountMaxValueStateTransformer(cryptoCurrencyStatus, yield))
}
override fun onCurrencyChangeClick(isFiat: Boolean) {
analyticsEventHandler.send(
StakingAnalyticsEvents.AmountSelectCurrency(value.cryptoCurrencySymbol, isFiat),
StakingAnalyticsEvent.AmountSelectCurrency(isFiat),
)
stateController.update(AmountCurrencyChangeStateTransformer(cryptoCurrencyStatus, isFiat))
}
override fun openValidators() {
analyticsEventHandler.send(
StakingAnalyticsEvents.ButtonValidator(
StakingAnalyticsEvent.ButtonValidator(
source = StakeScreenSource.Confirmation,
token = value.cryptoCurrencySymbol,
),
)
stakingStateRouter.showValidators()
@ -469,18 +459,15 @@ internal class StakingViewModel @Inject constructor(
override fun onValidatorSelect(validator: Yield.Validator) {
analyticsEventHandler.send(
StakingAnalyticsEvents.ValidatorChosen(
value.cryptoCurrencySymbol,
validator.name,
StakingAnalyticsEvent.ValidatorChosen(
validator = validator.name,
),
)
stateController.update(ValidatorSelectChangeTransformer(validator))
}
override fun openRewardsValidators() {
analyticsEventHandler.send(
StakingAnalyticsEvents.ButtonRewards(value.cryptoCurrencySymbol),
)
analyticsEventHandler.send(StakingAnalyticsEvent.ButtonRewards)
val rewardsValidators =
stateController.value.rewardsValidatorsState as? StakingStates.RewardsValidatorsState.Data
val rewards = rewardsValidators?.rewards
@ -488,9 +475,8 @@ internal class StakingViewModel @Inject constructor(
onActiveStake(rewards.first())
} else {
analyticsEventHandler.send(
StakingAnalyticsEvents.ButtonValidator(
StakingAnalyticsEvent.ButtonValidator(
source = StakeScreenSource.Info,
token = value.cryptoCurrencySymbol,
),
)
onNextClick(actionTypeToOverwrite = StakingActionCommonType.PENDING_REWARDS)
@ -542,9 +528,8 @@ internal class StakingViewModel @Inject constructor(
override fun onActiveStakeAnalytic() {
analyticsEventHandler.send(
StakingAnalyticsEvents.ButtonValidator(
StakingAnalyticsEvent.ButtonValidator(
source = StakeScreenSource.Info,
token = value.cryptoCurrencySymbol,
),
)
}
@ -595,7 +580,7 @@ internal class StakingViewModel @Inject constructor(
).fold(
ifLeft = { error ->
Timber.e(error.toString())
analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencySymbol))
analyticsEventHandler.send(StakingAnalyticsEvent.TransactionError)
stateController.update(
SetConfirmationStateAssentApprovalTransformer(
appCurrencyProvider = Provider { appCurrency },
@ -617,7 +602,7 @@ internal class StakingViewModel @Inject constructor(
).fold(
ifLeft = { error ->
Timber.e(error.toString())
analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencySymbol))
analyticsEventHandler.send(StakingAnalyticsEvent.TransactionError)
stateController.update(
SetConfirmationStateAssentApprovalTransformer(
appCurrencyProvider = Provider { appCurrency },
@ -747,7 +732,7 @@ internal class StakingViewModel @Inject constructor(
}
override fun onExploreClick() {
analyticsEventHandler.send(StakingAnalyticsEvents.ButtonExplore)
analyticsEventHandler.send(StakingAnalyticsEvent.ButtonExplore)
val confirmationDataState = uiState.value.confirmationState as? StakingStates.ConfirmationState.Data
val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content
val txUrl = transactionDoneState?.txUrl
@ -761,7 +746,7 @@ internal class StakingViewModel @Inject constructor(
val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content
val txUrl = transactionDoneState?.txUrl
analyticsEventHandler.send(StakingAnalyticsEvents.ButtonShare)
analyticsEventHandler.send(StakingAnalyticsEvent.ButtonShare)
if (txUrl != null) {
vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click)
stakingEventFactory.createShareDialog(txUrl = txUrl)
@ -838,10 +823,12 @@ internal class StakingViewModel @Inject constructor(
if (!isInitialInfoAnalyticSent) {
isInitialInfoAnalyticSent = true
val balances = status.value.yieldBalance as? YieldBalance.Data
paramsInterceptorHolder.addParamsInterceptor(
interceptor = StakingParamsInterceptor(status.currency.symbol),
)
analyticsEventHandler.send(
StakingAnalyticsEvents.StakingInfoScreenOpened(
StakingAnalyticsEvent.StakingInfoScreenOpened(
validatorsCount = balances?.getValidatorsCount() ?: 0,
token = status.currency.symbol,
),
)
}