Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-20 11:45:42 +03:00
parent 7b534690ff
commit 864d524841
10 changed files with 301 additions and 14 deletions

View file

@ -8,10 +8,7 @@ sealed class AnalyticsParam {
class Currency(currency: com.tangem.tap.features.wallet.models.Currency) : CurrencyType(currency.currencySymbol)
class Blockchain(blockchain: com.tangem.blockchain.common.Blockchain) : CurrencyType(blockchain.currency)
class Token(token: com.tangem.blockchain.common.Token) : CurrencyType(token.symbol)
class FiatCurrency(
fiatCurrency: com.tangem.tap.common.entities.FiatCurrency,
) : CurrencyType(fiatCurrency.code)
class FiatCurrency(fiatCurrency: com.tangem.tap.common.entities.FiatCurrency) : CurrencyType(fiatCurrency.code)
class Amount(amount: com.tangem.blockchain.common.Amount) : CurrencyType(amount.currencySymbol)
}

View file

@ -12,6 +12,10 @@ sealed class Portfolio(
) : AnalyticsEvent("Portfolio", event, params) {
class Refreshed : Portfolio("Refreshed")
class ButtonManageTokens : Portfolio("Button - Manage Tokens")
class TokenTapped : Portfolio("Token is Tapped")
class OrganizeTokens : Portfolio("Button - Organize Tokens")
}

View file

@ -1,18 +1,26 @@
package com.tangem.tap.features.saveWallet.ui
import androidx.lifecycle.ViewModel
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.saveWallet.redux.SaveWalletAction
import com.tangem.tap.features.saveWallet.redux.SaveWalletState
import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog
import com.tangem.tap.store
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
internal class SaveWalletViewModel : ViewModel(), StoreSubscriber<SaveWalletState> {
@HiltViewModel
internal class SaveWalletViewModel @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
) : ViewModel(), StoreSubscriber<SaveWalletState> {
private val stateInternal = MutableStateFlow(SaveWalletScreenState())
val state: StateFlow<SaveWalletScreenState> = stateInternal
@ -22,10 +30,12 @@ internal class SaveWalletViewModel : ViewModel(), StoreSubscriber<SaveWalletStat
}
fun saveWallet() {
analyticsEventHandler.send(WalletScreenAnalyticsEvent.EnableBiometrics(AnalyticsParam.OnOffState.On))
store.dispatch(SaveWalletAction.Save)
}
fun cancelOrClose() {
analyticsEventHandler.send(WalletScreenAnalyticsEvent.EnableBiometrics(AnalyticsParam.OnOffState.Off))
store.dispatch(SaveWalletAction.Dismiss)
}

View file

@ -0,0 +1,138 @@
package com.tangem.core.analytics.models
sealed class AnalyticsParam {
sealed class CardBalanceState(val value: String) {
object Empty : CardBalanceState("Empty")
object Full : CardBalanceState("Full")
object CustomToken : CardBalanceState("Custom token")
object BlockchainError : CardBalanceState("Blockchain error")
companion object
}
sealed class RateApp(val value: String) {
object Liked : RateApp("Liked")
object Disliked : RateApp("Disliked")
object Closed : RateApp("Close")
}
sealed class OnOffState(val value: String) {
object On : OnOffState("On")
object Off : OnOffState("Off")
}
sealed class OrganizeSortType(val value: String) {
object ByBalance : OrganizeSortType("By Balance")
object Manually : OrganizeSortType("Manually")
}
sealed class UserCode(val value: String) {
object AccessCode : UserCode("Access Code")
object Passcode : UserCode("Passcode")
}
sealed class AccessCodeRecoveryStatus(val value: String) {
val key: String = "Status"
object Enabled : AccessCodeRecoveryStatus("Enabled")
object Disabled : AccessCodeRecoveryStatus("Disabled")
companion object {
fun from(enabled: Boolean): AccessCodeRecoveryStatus {
return if (enabled) Enabled else Disabled
}
}
}
sealed class Error(val value: String) {
object App : Error("App Error")
object CardSdk : Error("Card Sdk Error")
object BlockchainSdk : Error("Blockchain Sdk Error")
}
sealed class ScannedFrom(val value: String) {
object Introduction : ScannedFrom("Introduction")
object Main : ScannedFrom("Main")
object SignIn : ScannedFrom("Sign In")
object MyWallets : ScannedFrom("My Wallets")
}
sealed class TxSentFrom(val value: String) {
data class Send(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
) : TxSentFrom("Send"), TxData
data class Swap(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
) : TxSentFrom("Swap"), TxData
data class Approve(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
val permissionType: String,
) : TxSentFrom("Approve"), TxData
object WalletConnect : TxSentFrom("WalletConnect")
object Sell : TxSentFrom("Sell")
}
sealed interface TxData {
val blockchain: String
val token: String
val feeType: FeeType
}
sealed class FeeType(val value: String) {
object Fixed : FeeType("Fixed")
object Min : FeeType("Min")
object Normal : FeeType("Normal")
object Max : FeeType("Max")
companion object {
fun fromString(feeType: String): FeeType {
return when (feeType) {
Min.value -> Min
Normal.value -> Normal
Max.value -> Max
Fixed.value -> Fixed
else -> Fixed
}
}
}
}
sealed class WalletCreationType(val value: String) {
object PrivateKey : WalletCreationType("Private key")
object NewSeed : WalletCreationType("New seed")
object SeedImport : WalletCreationType("Seed import")
}
companion object Key {
const val BLOCKCHAIN = "blockchain"
const val TOKEN = "Token"
const val SOURCE = "Source"
const val BALANCE = "Balance"
const val BATCH = "Batch"
const val FEE_TYPE = "Fee Type"
const val PERMISSION_TYPE = "Permission Type"
const val PRODUCT_TYPE = "Product Type"
const val FIRMWARE = "Firmware"
const val CURRENCY = "Currency"
const val ERROR_DESCRIPTION = "Error Description"
const val ERROR_CODE = "Error Code"
const val ERROR_KEY = "Error Key"
const val CREATION_TYPE = "Creation type"
const val DAPP_NAME = "DApp Name"
const val DAPP_URL = "DApp Url"
const val METHOD_NAME = "Method Name"
const val VALIDATION = "Validation"
const val BLOCKCHAIN_EXCEPTION_HOST = "exception_host"
const val BLOCKCHAIN_SELECTED_HOST = "selected_host"
}
}

View file

@ -44,6 +44,8 @@ dependencies {
implementation(projects.core.navigation)
implementation(projects.core.ui)
implementation(projects.core.utils)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
/** Domain modules */
implementation(projects.common)

View file

@ -1,10 +1,10 @@
package com.tangem.feature.wallet.presentation.organizetokens
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.*
import arrow.core.getOrElse
import com.tangem.common.Provider
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
@ -13,6 +13,7 @@ import com.tangem.domain.tokens.ToggleTokenListGroupingUseCase
import com.tangem.domain.tokens.ToggleTokenListSortingUseCase
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.presentation.organizetokens.analytics.PortfolioOrganizeTokensAnalyticsEvent
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
import com.tangem.feature.wallet.presentation.organizetokens.utils.CryptoCurrenciesIdsResolver
@ -35,9 +36,10 @@ internal class OrganizeTokensViewModel @Inject constructor(
private val toggleTokenListSortingUseCase: ToggleTokenListSortingUseCase,
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val analyticsEventsHandler: AnalyticsEventHandler,
private val dispatchers: CoroutineDispatcherProvider,
savedStateHandle: SavedStateHandle,
) : ViewModel(), OrganizeTokensIntents {
) : ViewModel(), DefaultLifecycleObserver, OrganizeTokensIntents {
lateinit var router: InnerWalletRouter
@ -68,11 +70,17 @@ internal class OrganizeTokensViewModel @Inject constructor(
val uiState: StateFlow<OrganizeTokensState> = stateHolder.stateFlow
override fun onCreate(owner: LifecycleOwner) {
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ScreenOpened)
}
override fun onBackClick() {
router.popBackStack()
}
override fun onSortClick() {
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance)
viewModelScope.launch(dispatchers.default) {
val list = tokenList ?: return@launch
@ -87,6 +95,8 @@ internal class OrganizeTokensViewModel @Inject constructor(
}
override fun onGroupClick() {
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group)
viewModelScope.launch(dispatchers.default) {
val list = tokenList ?: return@launch
@ -107,11 +117,19 @@ internal class OrganizeTokensViewModel @Inject constructor(
val listState = uiState.value.itemsState
val resolver = CryptoCurrenciesIdsResolver()
val isGroupedByNetwork = listState is OrganizeTokensListState.GroupedByNetwork
val isSortedByBalance = uiState.value.header.isSortedByBalance
sendAnalyticsEvent(
isGroupedByNetwork = isGroupedByNetwork,
isSortedByBalance = isSortedByBalance,
)
val result = applyTokenListSortingUseCase(
userWalletId = userWalletId,
sortedTokensIds = resolver.resolve(listState, tokenList),
isGroupedByNetwork = listState is OrganizeTokensListState.GroupedByNetwork,
isSortedByBalance = uiState.value.header.isSortedByBalance,
isGroupedByNetwork = isGroupedByNetwork,
isSortedByBalance = isSortedByBalance,
)
result.fold(
@ -127,6 +145,8 @@ internal class OrganizeTokensViewModel @Inject constructor(
}
override fun onCancelClick() {
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Cancel)
router.popBackStack()
}
@ -166,4 +186,21 @@ internal class OrganizeTokensViewModel @Inject constructor(
initialValue = AppCurrency.Default,
)
}
private fun sendAnalyticsEvent(isGroupedByNetwork: Boolean, isSortedByBalance: Boolean) {
analyticsEventsHandler.send(
PortfolioOrganizeTokensAnalyticsEvent.Apply(
grouping = if (isGroupedByNetwork) {
AnalyticsParam.OnOffState.On
} else {
AnalyticsParam.OnOffState.Off
},
organizeSortType = if (isSortedByBalance) {
AnalyticsParam.OrganizeSortType.ByBalance
} else {
AnalyticsParam.OrganizeSortType.Manually
},
),
)
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.feature.wallet.presentation.organizetokens.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
sealed class PortfolioOrganizeTokensAnalyticsEvent(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Portfolio / Organize Tokens", event, params) {
object ScreenOpened : PortfolioOrganizeTokensAnalyticsEvent("Organize Tokens Screen Opened")
object ByBalance : PortfolioOrganizeTokensAnalyticsEvent("Button - By Balance")
object Group : PortfolioOrganizeTokensAnalyticsEvent("Button - Group")
class Apply(
grouping: AnalyticsParam.OnOffState,
organizeSortType: AnalyticsParam.OrganizeSortType,
) : PortfolioOrganizeTokensAnalyticsEvent(
"Button - Apply",
params = mapOf(
"Group" to grouping.value,
"Sort" to organizeSortType.value,
),
)
object Cancel : PortfolioOrganizeTokensAnalyticsEvent("Button - Cancel")
}

View file

@ -0,0 +1,17 @@
package com.tangem.feature.wallet.presentation.wallet.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
sealed class PortfolioEvent(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Portfolio", event, params) {
object Refreshed : PortfolioEvent("Refreshed")
object ButtonManageTokens : PortfolioEvent("Button - Manage Tokens")
object TokenTapped : PortfolioEvent("Token is Tapped")
object OrganizeTokens : PortfolioEvent("Button - Organize Tokens")
}

View file

@ -0,0 +1,28 @@
package com.tangem.feature.wallet.presentation.wallet.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
sealed class WalletScreenAnalyticsEvent(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Main Screen", event, params) {
object ScreenOpened : WalletScreenAnalyticsEvent("Screen opened")
object WalletSwipe : WalletScreenAnalyticsEvent("Wallet Swipe")
class EnableBiometrics(state: AnalyticsParam.OnOffState) : WalletScreenAnalyticsEvent(
event = "Enable Biometric",
params = mapOf("State" to state.value),
)
// TODO [REDACTED_JIRA]
class NoticeRateAppButton(result: AnalyticsParam.RateApp) : WalletScreenAnalyticsEvent(
event = "Notice - Rate The App Button Tapped",
params = mapOf("Result" to result.value),
)
object NoticeBackupYourWalletTapped : WalletScreenAnalyticsEvent("Notice - Backup Your Wallet Tapped")
object NoticeScanYourCardTapped : WalletScreenAnalyticsEvent("Notice - Scan Your Card Tapped")
object NoticeWalletLocked : WalletScreenAnalyticsEvent("Notice - Wallet Locked")
}

View file

@ -6,6 +6,7 @@ import arrow.core.getOrElse
import com.tangem.common.Provider
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.navigation.AppScreen
import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
@ -37,6 +38,8 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.*
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
import com.tangem.feature.wallet.presentation.wallet.analytics.PortfolioEvent
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.feature.wallet.presentation.wallet.state.*
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletBottomSheetConfig
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState
@ -93,6 +96,7 @@ internal class WalletViewModel @Inject constructor(
private val walletManagersFacade: WalletManagersFacade,
private val reduxStateHolder: ReduxStateHolder,
private val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventsHandler: AnalyticsEventHandler,
// endregion Parameters
) : ViewModel(), DefaultLifecycleObserver, WalletClickIntents {
@ -141,6 +145,8 @@ internal class WalletViewModel @Inject constructor(
)
override fun onCreate(owner: LifecycleOwner) {
analyticsEventsHandler.send(WalletScreenAnalyticsEvent.ScreenOpened)
viewModelScope.launch(dispatchers.main) {
delay(timeMillis = 1_800)
@ -231,6 +237,8 @@ internal class WalletViewModel @Inject constructor(
}
override fun onScanCardClick() {
analyticsEventsHandler.send(WalletScreenAnalyticsEvent.NoticeScanYourCardTapped)
viewModelScope.launch(dispatchers.io) {
scanCardProcessor.scan()
.doOnSuccess {
@ -330,7 +338,10 @@ internal class WalletViewModel @Inject constructor(
override fun onDetailsClick() = router.openDetailsScreen()
override fun onBackupCardClick() = router.openOnboardingScreen()
override fun onBackupCardClick() {
analyticsEventsHandler.send(WalletScreenAnalyticsEvent.NoticeBackupYourWalletTapped)
router.openOnboardingScreen()
}
override fun onCriticalWarningAlreadySignedHashesClick() {
uiState = stateFactory.getStateWithOpenWalletBottomSheet(
@ -363,6 +374,8 @@ internal class WalletViewModel @Inject constructor(
}
override fun onWalletChange(index: Int) {
analyticsEventsHandler.send(WalletScreenAnalyticsEvent.WalletSwipe)
val state = uiState as? WalletState.ContentState ?: return
if (state.walletsListConfig.selectedWalletIndex == index) return
@ -417,8 +430,14 @@ internal class WalletViewModel @Inject constructor(
?: return
when (uiState) {
is WalletMultiCurrencyState.Content -> refreshMultiCurrencyContent(selectedWalletIndex)
is WalletSingleCurrencyState.Content -> refreshSingleCurrencyContent(selectedWalletIndex)
is WalletMultiCurrencyState.Content -> {
analyticsEventsHandler.send(PortfolioEvent.Refreshed)
refreshMultiCurrencyContent(selectedWalletIndex)
}
is WalletSingleCurrencyState.Content -> {
analyticsEventsHandler.send(PortfolioEvent.Refreshed)
refreshSingleCurrencyContent(selectedWalletIndex)
}
is WalletState.Initial,
is WalletMultiCurrencyState.Locked,
is WalletSingleCurrencyState.Locked,
@ -427,6 +446,8 @@ internal class WalletViewModel @Inject constructor(
}
override fun onOrganizeTokensClick() {
analyticsEventsHandler.send(PortfolioEvent.OrganizeTokens)
val state = requireNotNull(uiState as? WalletState.ContentState)
val index = state.walletsListConfig.selectedWalletIndex
val walletId = state.walletsListConfig.wallets[index].id
@ -533,6 +554,7 @@ internal class WalletViewModel @Inject constructor(
}
override fun onManageTokensClick() {
analyticsEventsHandler.send(PortfolioEvent.ButtonManageTokens)
reduxStateHolder.dispatch(action = TokensAction.SetArgs.ManageAccess)
router.openManageTokensScreen()
}
@ -567,6 +589,8 @@ internal class WalletViewModel @Inject constructor(
}
override fun onUnlockWalletClick() {
analyticsEventsHandler.send(WalletScreenAnalyticsEvent.NoticeWalletLocked)
viewModelScope.launch(dispatchers.io) {
unlockWalletsUseCase()
}
@ -586,6 +610,7 @@ internal class WalletViewModel @Inject constructor(
}
override fun onTokenItemClick(currency: CryptoCurrency) {
analyticsEventsHandler.send(PortfolioEvent.TokenTapped)
router.openTokenDetails(currency = currency)
}