From b1c2f8d6e3f6a6b0b02e381d2be47a482789c8fe Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Oct 2023 16:43:08 +0800 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/common/analytics/events/Basic.kt | 16 +++-- .../common/redux/global/GlobalMiddleware.kt | 4 ++ .../saveWallet/ui/SaveWalletViewModel.kt | 6 +- .../tap/features/wallet/ui/WalletViewModel.kt | 1 + .../welcome/redux/WelcomeMiddleware.kt | 28 ++++++++ .../tangem/domain/common/CardTypesResolver.kt | 4 -- .../domain/common/TangemCardTypesResolver.kt | 4 -- .../analytics/WalletScreenAnalyticsEvent.kt | 61 +++++++++++----- .../wallet/ui/utils/ScrollOffsetCollector.kt | 19 +++-- .../wallet/viewmodels/WalletViewModel.kt | 69 ++++++++++++++++--- 10 files changed, 158 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt index 2782ceac8f..ade60b95cf 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt @@ -32,14 +32,18 @@ sealed class Basic( batch: String, signInType: SignInType, walletsCount: String, + hasBackup: Boolean?, ) : Basic( event = "Signed in", - params = mapOf( - AnalyticsParam.CURRENCY to currency.value, - AnalyticsParam.BATCH to batch, - "Sign in type" to signInType.name, - "Wallets Count" to walletsCount, - ), + params = buildMap { + put(AnalyticsParam.CURRENCY, currency.value) + put(AnalyticsParam.BATCH, batch) + put("Sign in type", signInType.name) + put("Wallets Count", walletsCount) + if (hasBackup != null) { + put("Backuped", if (hasBackup) "Yes" else "No") + } + }, ) { enum class SignInType { Card, Biometric diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt index e3c134ee2d..3ce7fcd492 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt @@ -3,10 +3,12 @@ package com.tangem.tap.common.redux.global import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.guard +import com.tangem.core.analytics.Analytics import com.tangem.datasource.config.models.Config import com.tangem.domain.common.LogConfig import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse +import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.extensions.dispatchDebugErrorNotification import com.tangem.tap.common.extensions.dispatchDialogShow @@ -182,6 +184,8 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di action.manager.selectedUserWallet .distinctUntilChanged() .onEach { userWallet -> + Analytics.send(event = Basic.WalletOpened()) + store.state.globalState.feedbackManager?.infoHolder?.let { infoHolder -> infoHolder.setCardInfo(data = userWallet.scanResponse) diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt index 67cdd2c0bd..4c56a3b9b4 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt @@ -30,12 +30,14 @@ internal class SaveWalletViewModel @Inject constructor( } fun saveWallet() { - analyticsEventHandler.send(WalletScreenAnalyticsEvent.EnableBiometrics(AnalyticsParam.OnOffState.On)) + analyticsEventHandler.send(WalletScreenAnalyticsEvent.MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.On)) store.dispatch(SaveWalletAction.Save) } fun cancelOrClose() { - analyticsEventHandler.send(WalletScreenAnalyticsEvent.EnableBiometrics(AnalyticsParam.OnOffState.Off)) + analyticsEventHandler.send( + WalletScreenAnalyticsEvent.MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.Off), + ) store.dispatch(SaveWalletAction.Dismiss) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletViewModel.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletViewModel.kt index b94cc9de26..271bc8aaae 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletViewModel.kt @@ -64,6 +64,7 @@ internal class WalletViewModel @Inject constructor( batch = scanResponse.card.batchId, signInType = signInType, walletsCount = store.state.globalState.userWalletsListManager?.walletsCount.toString(), + hasBackup = scanResponse.card.backupStatus?.isActive, ), ) } diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index 164ef24c14..482671ce51 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -6,12 +6,15 @@ import com.tangem.common.doOnFailure import com.tangem.common.doOnResult import com.tangem.common.doOnSuccess import com.tangem.common.flatMap +import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.domain.wallets.legacy.unlockIfLockable import com.tangem.tap.* +import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.extensions.dispatchOnMain @@ -88,6 +91,11 @@ internal class WelcomeMiddleware { store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics.Error(error)) } .doOnSuccess { selectedUserWallet -> + sendSignedInAnalyticsEvent( + scanResponse = selectedUserWallet.scanResponse, + signInType = Basic.SignedIn.SignInType.Biometric, + ) + store.dispatchWithMain(SignInAction.SetSignInType(Basic.SignedIn.SignInType.Biometric)) store.dispatchWithMain(NavigationAction.NavigateTo(AppScreen.Wallet)) store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics.Success) @@ -116,6 +124,8 @@ internal class WelcomeMiddleware { store.dispatchWithMain(WelcomeAction.ProceedWithCard.Error(error)) } .doOnSuccess { + sendSignedInAnalyticsEvent(scanResponse = scanResponse, signInType = Basic.SignedIn.SignInType.Card) + store.dispatchWithMain(SignInAction.SetSignInType(Basic.SignedIn.SignInType.Card)) store.dispatchWithMain(NavigationAction.NavigateTo(AppScreen.Wallet)) store.dispatchWithMain(WelcomeAction.ProceedWithCard.Success) @@ -128,6 +138,24 @@ internal class WelcomeMiddleware { } } + private fun sendSignedInAnalyticsEvent(scanResponse: ScanResponse, signInType: Basic.SignedIn.SignInType) { + val currency = ParamCardCurrencyConverter().convert( + value = scanResponse.cardTypesResolver, + ) + + if (currency != null) { + Analytics.send( + event = Basic.SignedIn( + currency = currency, + batch = scanResponse.card.batchId, + signInType = signInType, + walletsCount = store.state.globalState.userWalletsListManager?.walletsCount.toString(), + hasBackup = scanResponse.card.backupStatus?.isActive, + ), + ) + } + } + private suspend fun disableUserWalletsSaving() { userWalletsListManager.clear() .flatMap { walletStoresManager.clear() } diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt index 55153e8edc..4b409286eb 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt @@ -38,8 +38,4 @@ interface CardTypesResolver { fun isAttestationFailed(): Boolean fun hasWalletSignedHashes(): Boolean - - fun hasBackup(): Boolean - - fun isBackupForbidden(): Boolean } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt index 2a4baa2814..35164b37d6 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt @@ -92,10 +92,6 @@ internal class TangemCardTypesResolver( } } - override fun hasBackup(): Boolean = card.backupStatus != CardDTO.BackupStatus.NoBackup - - override fun isBackupForbidden(): Boolean = !(card.settings.isBackupAllowed || card.settings.isHDWalletAllowed) - private fun Blockchain.Companion.fromBlockchainName(blockchainName: String): Blockchain { // workaround for BSC (BNB) notes cards return when (blockchainName) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index a1b279fb6c..8d43c5d586 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -3,25 +3,52 @@ 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 = mapOf(), -) : AnalyticsEvent("Main Screen", event, params) { +sealed class WalletScreenAnalyticsEvent { - object ScreenOpened : WalletScreenAnalyticsEvent("Screen opened") - object WalletSwipe : WalletScreenAnalyticsEvent("Wallet Swipe") + sealed class Basic( + event: String, + params: Map = mapOf(), + error: Throwable? = null, + ) : AnalyticsEvent(category = "Basic", event = event, params = params, error = error) { - class EnableBiometrics(state: AnalyticsParam.OnOffState) : WalletScreenAnalyticsEvent( - event = "Enable Biometric", - params = mapOf("State" to state.value), - ) + object WalletOpened : Basic(event = "Wallet Opened") - class NoticeRateAppButton(result: AnalyticsParam.RateApp) : WalletScreenAnalyticsEvent( - event = "Notice - Rate The App Button Tapped", - params = mapOf("Result" to result.value), - ) + class CardWasScanned(source: AnalyticsParam.ScannedFrom) : Basic( + event = "Card Was Scanned", + params = mapOf( + AnalyticsParam.SOURCE to source.value, + ), + ) - object NoticeBackupYourWalletTapped : WalletScreenAnalyticsEvent("Notice - Backup Your Wallet Tapped") - object NoticeScanYourCardTapped : WalletScreenAnalyticsEvent("Notice - Scan Your Card Tapped") - object NoticeWalletLocked : WalletScreenAnalyticsEvent("Notice - Wallet Locked") + class BalanceLoaded(balance: AnalyticsParam.CardBalanceState) : Basic( + event = "Balance Loaded", + params = mapOf( + AnalyticsParam.BALANCE to balance.value, + ), + ) + } + + sealed class MainScreen( + event: String, + params: Map = mapOf(), + ) : AnalyticsEvent(category = "Main Screen", event = event, params = params) { + + object ScreenOpened : MainScreen(event = "Screen opened") + object WalletSwipe : MainScreen(event = "Wallet Swipe") + + class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreen( + event = "Enable Biometric", + params = mapOf("State" to state.value), + ) + + class NoticeRateAppButton(result: AnalyticsParam.RateApp) : MainScreen( + event = "Notice - Rate The App Button Tapped", + params = mapOf("Result" to result.value), + ) + + object NoticeBackupYourWalletTapped : MainScreen(event = "Notice - Backup Your Wallet Tapped") + object NoticeScanYourCardTapped : MainScreen(event = "Notice - Scan Your Card Tapped") + object NoticeWalletLocked : MainScreen(event = "Notice - Wallet Locked") + object WalletUnlockTapped : MainScreen(event = "Button - Wallet Unlock Tapped") + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollector.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollector.kt index 60f81583b2..2119079f09 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollector.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollector.kt @@ -24,14 +24,10 @@ internal class ScrollOffsetCollector( private val isAutoScroll: State, ) : FlowCollector> { - private val LazyListItemInfo.halfItemSize get() = size.div(other = 2) + private val LazyListItemInfo.halfItemSize + get() = size.div(other = 2) private var currentIndex = walletsListConfig.selectedWalletIndex - set(value) { - if (field != value) { - field = value - } - } override suspend fun emit(value: List) { // Auto scroll must not change wallet @@ -46,11 +42,14 @@ internal class ScrollOffsetCollector( val lastItem = value.lastOrNull() ?: return if (abs(firstItem.offset) > firstItem.halfItemSize) { - val newIndex = firstItem.index + 1 - currentIndex = newIndex - walletsListConfig.onWalletChange(newIndex) + onWalletChange(newIndex = firstItem.index + 1) } else if (abs(lastItem.offset) > lastItem.halfItemSize) { - val newIndex = lastItem.index - 1 + onWalletChange(newIndex = lastItem.index - 1) + } + } + + private fun onWalletChange(newIndex: Int) { + if (currentIndex != newIndex) { currentIndex = newIndex walletsListConfig.onWalletChange(newIndex) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index fa4de5b71a..bafb722692 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -77,6 +77,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.math.BigDecimal import javax.inject.Inject import kotlin.properties.Delegates @@ -185,7 +186,7 @@ internal class WalletViewModel @Inject constructor( ) override fun onCreate(owner: LifecycleOwner) { - analyticsEventsHandler.send(WalletScreenAnalyticsEvent.ScreenOpened) + analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened) viewModelScope.launch(dispatchers.main) { delay(timeMillis = 1_800) @@ -301,7 +302,8 @@ internal class WalletViewModel @Inject constructor( override fun onGenerateMissedAddressesClick(missedAddressCurrencies: List) { val state = uiState as? WalletState.ContentState ?: return - analyticsEventsHandler.send(WalletScreenAnalyticsEvent.NoticeScanYourCardTapped) + analyticsEventsHandler.send(WalletScreenAnalyticsEvent.Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Main)) + analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.NoticeScanYourCardTapped) viewModelScope.launch(dispatchers.io) { val userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex) @@ -401,6 +403,9 @@ internal class WalletViewModel @Inject constructor( override fun onScanToUnlockWalletClick() { val state = uiState as? WalletState.ContentState ?: return + + analyticsEventsHandler.send(event = WalletScreenAnalyticsEvent.MainScreen.WalletUnlockTapped) + val lockedWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex) viewModelScope.launch(dispatchers.io) { @@ -419,7 +424,7 @@ internal class WalletViewModel @Inject constructor( override fun onDetailsClick() = router.openDetailsScreen() override fun onBackupCardClick() { - analyticsEventsHandler.send(WalletScreenAnalyticsEvent.NoticeBackupYourWalletTapped) + analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.NoticeBackupYourWalletTapped) router.openOnboardingScreen() } @@ -433,7 +438,9 @@ internal class WalletViewModel @Inject constructor( } override fun onLikeAppClick() { - analyticsEventsHandler.send(WalletScreenAnalyticsEvent.NoticeRateAppButton(AnalyticsParam.RateApp.Liked)) + analyticsEventsHandler.send( + WalletScreenAnalyticsEvent.MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Liked), + ) uiState = stateFactory.getStateAndTriggerEvent( state = uiState, event = WalletEvent.RateApp( @@ -448,7 +455,9 @@ internal class WalletViewModel @Inject constructor( } override fun onDislikeAppClick() { - analyticsEventsHandler.send(WalletScreenAnalyticsEvent.NoticeRateAppButton(AnalyticsParam.RateApp.Disliked)) + analyticsEventsHandler.send( + WalletScreenAnalyticsEvent.MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Disliked), + ) viewModelScope.launch(dispatchers.main) { neverToSuggestRateAppUseCase() @@ -457,19 +466,20 @@ internal class WalletViewModel @Inject constructor( } override fun onCloseRateAppNotificationClick() { - analyticsEventsHandler.send(WalletScreenAnalyticsEvent.NoticeRateAppButton(AnalyticsParam.RateApp.Closed)) + analyticsEventsHandler.send( + WalletScreenAnalyticsEvent.MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Closed), + ) viewModelScope.launch(dispatchers.main) { remindToRateAppLaterUseCase() } } override fun onWalletChange(index: Int) { - analyticsEventsHandler.send(WalletScreenAnalyticsEvent.WalletSwipe) - val state = uiState as? WalletState.ContentState ?: return if (state.walletsListConfig.selectedWalletIndex == index) return changeCardAnalyticsContextUseCase(scanResponse = getWallet(index).scanResponse) + analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.WalletSwipe) // Reset the job to avoid a redundant state updating onWalletChangeJobHolder.update(null) @@ -769,7 +779,7 @@ internal class WalletViewModel @Inject constructor( override fun onUnlockWalletClick() { val state = uiState as? WalletState.ContentState ?: return - analyticsEventsHandler.send(WalletScreenAnalyticsEvent.NoticeWalletLocked) + analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.NoticeWalletLocked) viewModelScope.launch(dispatchers.main) { unlockWalletsUseCase( @@ -992,7 +1002,10 @@ internal class WalletViewModel @Inject constructor( .onEach { maybeTokenList -> uiState = stateFactory.getStateByTokensList(maybeTokenList.getTokenListWithWallet(wallet)) - maybeTokenList.onRight { checkMultiWalletWithFunds(it) } + maybeTokenList.onRight { + analyticsEventsHandler.sendBalanceLoadedEvent(fiatBalance = it.totalFiatBalance) + checkMultiWalletWithFunds(it) + } updateNotifications( index = walletIndex, @@ -1095,6 +1108,17 @@ internal class WalletViewModel @Inject constructor( uiState = stateFactory.getSingleCurrencyLoadedBalanceState(maybeCryptoCurrencyStatus) maybeCryptoCurrencyStatus.onRight { status -> + val fiatAmount = status.value.fiatAmount + analyticsEventsHandler.send( + event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded( + balance = when { + fiatAmount == null -> AnalyticsParam.CardBalanceState.BlockchainError + fiatAmount.isZero() -> AnalyticsParam.CardBalanceState.Empty + else -> AnalyticsParam.CardBalanceState.Full + }, + ), + ) + singleWalletCryptoCurrencyStatus = status if (status.value.amount?.isZero() == false) { @@ -1123,7 +1147,10 @@ internal class WalletViewModel @Inject constructor( .onEach { maybeTokenList -> uiState = stateFactory.getStateByTokensList(maybeTokenList.getTokenListWithWallet(wallet)) - maybeTokenList.onRight { checkMultiWalletWithFunds(it) } + maybeTokenList.onRight { tokenList -> + analyticsEventsHandler.sendBalanceLoadedEvent(tokenList.totalFiatBalance) + checkMultiWalletWithFunds(tokenList) + } updateNotifications( index = walletIndex, @@ -1135,6 +1162,26 @@ internal class WalletViewModel @Inject constructor( .saveIn(tokensJobHolder) } + private fun AnalyticsEventHandler.sendBalanceLoadedEvent(fiatBalance: TokenList.FiatBalance) { + val cardBalanceState = when (fiatBalance) { + is TokenList.FiatBalance.Failed -> AnalyticsParam.CardBalanceState.BlockchainError + is TokenList.FiatBalance.Loaded -> { + if (fiatBalance.amount > BigDecimal.ZERO) { + AnalyticsParam.CardBalanceState.Full + } else if (fiatBalance.amount.isZero()) { + AnalyticsParam.CardBalanceState.Empty + } else { + null + } + } + else -> null + } + + cardBalanceState?.let { + send(event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded(balance = it)) + } + } + private fun updateTxHistory(userWalletId: UserWalletId, currency: CryptoCurrency, refresh: Boolean) { viewModelScope.launch(dispatchers.io) { val txHistoryItemsCountEither = txHistoryItemsCountUseCase(