Updated on 2026-08-14

This commit is contained in:
Tangem 2023-12-07 13:19:03 +04:00
parent a9dfa54eec
commit 9647e6caa3
31 changed files with 302 additions and 40 deletions

View file

@ -51,6 +51,7 @@ dependencies {
implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models) implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.transaction) implementation(projects.domain.transaction)
implementation(projects.domain.analytics)
implementation(projects.common) implementation(projects.common)
implementation(projects.core.analytics) implementation(projects.core.analytics)

View file

@ -2,16 +2,18 @@ package com.tangem.tap.common.analytics.converters
import com.tangem.common.Converter import com.tangem.common.Converter
import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.Basic
/** /**
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
class TopUpEventConverter : Converter<CardTypesResolver, Basic.ToppedUp?> { class TopUpEventConverter : Converter<Pair<UserWalletId, CardTypesResolver>, Basic.ToppedUp?> {
override fun convert(value: CardTypesResolver): Basic.ToppedUp? { override fun convert(value: Pair<UserWalletId, CardTypesResolver>): Basic.ToppedUp? {
val paramCardCurrency = ParamCardCurrencyConverter().convert(value) ?: return null val (userWalletId, resolver) = value
val paramCardCurrency = ParamCardCurrencyConverter().convert(resolver) ?: return null
return Basic.ToppedUp(paramCardCurrency) return Basic.ToppedUp(userWalletId, paramCardCurrency)
} }
} }

View file

@ -14,7 +14,7 @@ sealed class AnalyticsParam {
// MultiCurrency or CurrencyType // MultiCurrency or CurrencyType
sealed class CardCurrency(val value: String) { sealed class CardCurrency(val value: String) {
object MultiCurrency : CardCurrency("Multicurrency") object MultiCurrency : CardCurrency(value = "Multicurrency")
class SingleCurrency(type: CurrencyType) : CardCurrency(type.value) class SingleCurrency(type: CurrencyType) : CardCurrency(type.value)
} }

View file

@ -1,6 +1,8 @@
package com.tangem.tap.common.analytics.events package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.OneTimeAnalyticsEvent
import com.tangem.domain.wallets.models.UserWalletId
/** /**
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
@ -50,10 +52,15 @@ sealed class Basic(
} }
} }
class ToppedUp(currency: AnalyticsParam.CardCurrency) : Basic( class ToppedUp(userWalletId: UserWalletId, currency: AnalyticsParam.CardCurrency) :
event = "Topped up", Basic(
params = mapOf(AnalyticsParam.CURRENCY to currency.value), event = "Topped up",
) params = mapOf(AnalyticsParam.CURRENCY to currency.value),
),
OneTimeAnalyticsEvent {
override val oneTimeEventId: String = id + userWalletId.stringValue
}
class TransactionSent(sentFrom: AnalyticsParam.TxSentFrom, memoType: MemoType) : Basic( class TransactionSent(sentFrom: AnalyticsParam.TxSentFrom, memoType: MemoType) : Basic(
event = "Transaction sent", event = "Transaction sent",

View file

@ -154,7 +154,7 @@ class TopUpController(
if (cardBalanceState.isToppedUp()) { if (cardBalanceState.isToppedUp()) {
topupWalletStorage.save(topupInfo.copy(cardBalanceState = DataSourceTopupInfo.CardBalanceState.Full)) topupWalletStorage.save(topupInfo.copy(cardBalanceState = DataSourceTopupInfo.CardBalanceState.Full))
TopUpEventConverter().convert(cardTypesResolver)?.let { TopUpEventConverter().convert(value = userWalletId to cardTypesResolver)?.let {
Analytics.send(it) Analytics.send(it)
} }
} }

View file

@ -0,0 +1,18 @@
package com.tangem.tap.di.domain
import com.tangem.domain.analytics.CheckIsWalletToppedUpUseCase
import com.tangem.domain.analytics.repository.AnalyticsRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
@Module
@InstallIn(ViewModelComponent::class)
internal object AnalyticsDomainModule {
@Provides
fun provideCheckIsWalletToppedUpUseCase(analyticsRepository: AnalyticsRepository): CheckIsWalletToppedUpUseCase {
return CheckIsWalletToppedUpUseCase(analyticsRepository)
}
}

View file

@ -11,7 +11,10 @@ import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.domain.userwallets.UserWalletIdBuilder
import com.tangem.tap.* import com.tangem.tap.*
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.extensions.removeContext import com.tangem.tap.common.extensions.removeContext
@ -117,6 +120,15 @@ object OnboardingHelper {
Analytics.removeContext() Analytics.removeContext()
} }
fun sendToppedUpEvent(scanResponse: ScanResponse) {
val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
val currency = ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver)
if (userWalletId != null && currency != null) {
Analytics.send(Basic.ToppedUp(userWalletId, currency))
}
}
private suspend fun proceedWithScanResponse(scanResponse: ScanResponse, backupCardsIds: List<String>?) { private suspend fun proceedWithScanResponse(scanResponse: ScanResponse, backupCardsIds: List<String>?) {
val userWallet = UserWalletBuilder(scanResponse) val userWallet = UserWalletBuilder(scanResponse)
.backupCardsIds(backupCardsIds?.toSet()) .backupCardsIds(backupCardsIds?.toSet())

View file

@ -152,6 +152,8 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
} }
is OnboardingNoteAction.Balance.Set -> { is OnboardingNoteAction.Balance.Set -> {
if (action.balance.balanceIsToppedUp()) { if (action.balance.balanceIsToppedUp()) {
OnboardingHelper.sendToppedUpEvent(scanResponse)
store.state.globalState.topUpController?.send(scanResponse, AnalyticsParam.CardBalanceState.Full) store.state.globalState.topUpController?.send(scanResponse, AnalyticsParam.CardBalanceState.Full)
store.dispatch(OnboardingNoteAction.SetStepOfScreen(OnboardingNoteStep.Done)) store.dispatch(OnboardingNoteAction.SetStepOfScreen(OnboardingNoteStep.Done))
} }

View file

@ -260,6 +260,8 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
} }
is TwinCardsAction.Balance.Set -> { is TwinCardsAction.Balance.Set -> {
if (action.balance.balanceIsToppedUp()) { if (action.balance.balanceIsToppedUp()) {
OnboardingHelper.sendToppedUpEvent(getScanResponse())
store.state.globalState.topUpController?.send(getScanResponse(), AnalyticsParam.CardBalanceState.Full) store.state.globalState.topUpController?.send(getScanResponse(), AnalyticsParam.CardBalanceState.Full)
store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Done)) store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Done))
} }

View file

@ -10,9 +10,15 @@ dependencies {
implementation(deps.hilt.core) implementation(deps.hilt.core)
kapt(deps.hilt.kapt) kapt(deps.hilt.kapt)
/** Core shouldn't depends on core, but in case with utils and logging its necessary */ /** Analytics - Models */
implementation(projects.core.utils)
implementation(projects.core.analytics.models) implementation(projects.core.analytics.models)
/** Domain */
implementation(projects.domain.analytics)
/** Other */
implementation(deps.kotlin.coroutines) implementation(deps.kotlin.coroutines)
/** Core shouldn't depend on core, but in case with utils and logging its necessary */
implementation(projects.core.utils)
} }

View file

@ -114,6 +114,11 @@ sealed class AnalyticsParam {
object SeedImport : WalletCreationType("Seed import") object SeedImport : WalletCreationType("Seed import")
} }
sealed class WalletType(val value: String) {
object MultiCurrency : WalletType(value = "Multicurrency")
class SingleCurrency(currencyName: String) : WalletType(currencyName)
}
companion object Key { companion object Key {
const val BLOCKCHAIN = "blockchain" const val BLOCKCHAIN = "blockchain"
const val TOKEN = "Token" const val TOKEN = "Token"

View file

@ -3,7 +3,7 @@ package com.tangem.core.analytics.di
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.filter.OneTimeEventFilter import com.tangem.core.analytics.filter.OneTimeEventFilter
import com.tangem.core.analytics.repository.AnalyticsRepository import com.tangem.domain.analytics.repository.AnalyticsRepository
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn

View file

@ -4,7 +4,7 @@ import com.tangem.core.analytics.api.AnalyticsEventFilter
import com.tangem.core.analytics.api.AnalyticsHandler import com.tangem.core.analytics.api.AnalyticsHandler
import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.OneTimeAnalyticsEvent import com.tangem.core.analytics.models.OneTimeAnalyticsEvent
import com.tangem.core.analytics.repository.AnalyticsRepository import com.tangem.domain.analytics.repository.AnalyticsRepository
class OneTimeEventFilter( class OneTimeEventFilter(
private val analyticsRepository: AnalyticsRepository, private val analyticsRepository: AnalyticsRepository,

View file

@ -1,8 +0,0 @@
package com.tangem.core.analytics.repository
interface AnalyticsRepository {
suspend fun checkIsEventSent(eventId: String): Boolean
suspend fun setIsEventSent(eventId: String)
}

View file

@ -44,12 +44,20 @@ class AppPreferencesStore(
return this[key]?.let(adapter::fromJson) return this[key]?.let(adapter::fromJson)
} }
/** Get nullable list of data [T] by string [key] */ /** Get list of data [T] by string [key] */
inline fun <reified T> MutablePreferences.getObjectList(key: Preferences.Key<String>): List<T>? { inline fun <reified T> MutablePreferences.getObjectList(key: Preferences.Key<String>): List<T>? {
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java)) val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
return this[key]?.let(adapter::fromJson) return this[key]?.let(adapter::fromJson)
} }
/** Get map with [String] key and value [V] by string [key] from [MutablePreferences] */
inline fun <reified V> MutablePreferences.getObjectMap(key: Preferences.Key<String>): Map<String, V>? {
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)
return this[key]?.let(adapter::fromJson)
}
/** /**
* Set data [T] by string [key] to [MutablePreferences] * Set data [T] by string [key] to [MutablePreferences]
* *
@ -67,4 +75,12 @@ class AppPreferencesStore(
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java)) val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
this[key] = adapter.toJson(value) this[key] = adapter.toJson(value)
} }
/** Set map with [String] key and value [V] by string [key] to [MutablePreferences] */
inline fun <reified V> MutablePreferences.setObjectMap(key: Preferences.Key<String>, value: Map<String, V>) {
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)
this[key] = adapter.toJson(value)
}
} }

View file

@ -36,6 +36,8 @@ object PreferencesKeys {
val WALLETS_SCROLL_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "walletsScrollPreview") } val WALLETS_SCROLL_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "walletsScrollPreview") }
val SENT_ONE_TIME_EVENTS_KEY by lazy { stringPreferencesKey(name = "sentOneTimeEvents") } val SENT_ONE_TIME_EVENTS_KEY by lazy { stringPreferencesKey(name = "sentOneTimeEvents") }
val WALLETS_BALANCES_STATES_KEY by lazy { stringPreferencesKey(name = "walletsBalancesStates") }
} }
/** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore<Preferences> */ /** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore<Preferences> */

View file

@ -76,9 +76,31 @@ inline fun <reified T> AppPreferencesStore.getObjectList(key: Preferences.Key<St
return data.map { it[key]?.let(adapter::fromJson) } return data.map { it[key]?.let(adapter::fromJson) }
} }
/** Get nullable list of data [T] by string [key] */ /** Get list of data [T] by string [key], or empty if data is not found */
suspend inline fun <reified T> AppPreferencesStore.getObjectListSync(key: Preferences.Key<String>): List<T> { suspend inline fun <reified T> AppPreferencesStore.getObjectListSync(key: Preferences.Key<String>): List<T> {
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java)) val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
return data.firstOrNull()
?.get(key)
?.let(adapter::fromJson)
.orEmpty()
}
/** Store map with [String] key and value [V] by string [key] */
suspend inline fun <reified V> AppPreferencesStore.storeObjectMap(
key: Preferences.Key<String>,
value: Map<String, V>,
) {
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)
edit { it[key] = adapter.toJson(value) }
}
/** Get map with [String] key and value [V] by string [key], or empty if data is not found */
suspend inline fun <reified V> AppPreferencesStore.getObjectMap(key: Preferences.Key<String>): Map<String, V> {
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)
return data.firstOrNull() return data.firstOrNull()
?.get(key) ?.get(key)
?.let(adapter::fromJson) ?.let(adapter::fromJson)

View file

@ -11,8 +11,11 @@ android {
dependencies { dependencies {
/** Project - Domain */
implementation(projects.domain.analytics)
implementation(projects.domain.wallets.models)
/** Project - Analytics */ /** Project - Analytics */
implementation(projects.core.analytics)
implementation(projects.core.analytics.models) implementation(projects.core.analytics.models)
/** Project - Data */ /** Project - Data */

View file

@ -1,9 +1,12 @@
package com.tangem.data.analytics package com.tangem.data.analytics
import com.tangem.core.analytics.repository.AnalyticsRepository
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectListSync import com.tangem.datasource.local.preferences.utils.getObjectListSync
import com.tangem.datasource.local.preferences.utils.getObjectMap
import com.tangem.domain.analytics.model.WalletBalanceState
import com.tangem.domain.analytics.repository.AnalyticsRepository
import com.tangem.domain.wallets.models.UserWalletId
internal class DefaultAnalyticsRepository( internal class DefaultAnalyticsRepository(
private val appPreferencesStore: AppPreferencesStore, private val appPreferencesStore: AppPreferencesStore,
@ -18,10 +21,30 @@ internal class DefaultAnalyticsRepository(
override suspend fun setIsEventSent(eventId: String) { override suspend fun setIsEventSent(eventId: String) {
appPreferencesStore.editData { mutablePreferences -> appPreferencesStore.editData { mutablePreferences ->
val sentEvents = mutablePreferences.getObject<List<String>>(PreferencesKeys.SENT_ONE_TIME_EVENTS_KEY) val sentEvents = mutablePreferences.getObjectList<String>(PreferencesKeys.SENT_ONE_TIME_EVENTS_KEY)
val updatedSentEvents = sentEvents.orEmpty() + eventId val updatedSentEvents = sentEvents.orEmpty() + eventId
mutablePreferences.setObject(PreferencesKeys.SENT_ONE_TIME_EVENTS_KEY, updatedSentEvents) mutablePreferences.setObjectList(PreferencesKeys.SENT_ONE_TIME_EVENTS_KEY, updatedSentEvents)
}
}
override suspend fun getWalletBalanceState(userWalletId: UserWalletId): WalletBalanceState? {
val walletsBalanceState = appPreferencesStore.getObjectMap<WalletBalanceState>(
key = PreferencesKeys.WALLETS_BALANCES_STATES_KEY,
)
return walletsBalanceState[userWalletId.stringValue]
}
override suspend fun setWalletBalanceState(userWalletId: UserWalletId, balanceState: WalletBalanceState) {
appPreferencesStore.editData {
val walletsBalanceState = it.getObjectMap<WalletBalanceState>(
key = PreferencesKeys.WALLETS_BALANCES_STATES_KEY,
)
val updatedWalletsBalanceState = walletsBalanceState.orEmpty()
.plus(pair = userWalletId.stringValue to balanceState)
it.setObjectMap(PreferencesKeys.WALLETS_BALANCES_STATES_KEY, updatedWalletsBalanceState)
} }
} }
} }

View file

@ -1,8 +1,8 @@
package com.tangem.data.analytics.di package com.tangem.data.analytics.di
import com.tangem.core.analytics.repository.AnalyticsRepository
import com.tangem.data.analytics.DefaultAnalyticsRepository import com.tangem.data.analytics.DefaultAnalyticsRepository
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.analytics.repository.AnalyticsRepository
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn

1
domain/analytics/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,12 @@
plugins {
alias(deps.plugins.kotlin.jvm)
id("configuration")
}
dependencies {
/** Project - Domain */
implementation(projects.core.utils)
implementation(projects.domain.core)
implementation(projects.domain.wallets.models)
}

View file

@ -0,0 +1,56 @@
package com.tangem.domain.analytics
import arrow.core.Either
import arrow.core.raise.Raise
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.domain.analytics.model.WalletBalanceState
import com.tangem.domain.analytics.repository.AnalyticsRepository
import com.tangem.domain.wallets.models.UserWalletId
/**
* Check if wallet has been topped up now.
* */
class CheckIsWalletToppedUpUseCase(
private val analyticsRepository: AnalyticsRepository,
) {
/**
* Check if wallet has been topped up now.
*
* @param userWalletId User wallet ID.
* @param balanceState Current wallet balance.
*
* @return Either [Throwable] or [Boolean] representing if wallet has been topped up.
* */
suspend operator fun invoke(
userWalletId: UserWalletId,
balanceState: WalletBalanceState,
): Either<Throwable, Boolean> = either {
val storedBalanceState = getStoredBalanceState(userWalletId)
when {
storedBalanceState == WalletBalanceState.ToppedUp -> false // already topped up
storedBalanceState == null && balanceState == WalletBalanceState.ToppedUp -> {
setBalanceState(userWalletId, balanceState)
false // already topped up
}
else -> {
setBalanceState(userWalletId, balanceState)
balanceState == WalletBalanceState.ToppedUp
}
}
}
private suspend fun Raise<Throwable>.getStoredBalanceState(userWalletId: UserWalletId) = catch(
block = { analyticsRepository.getWalletBalanceState(userWalletId) },
catch = ::raise,
)
private suspend fun Raise<Throwable>.setBalanceState(userWalletId: UserWalletId, balanceState: WalletBalanceState) {
catch(
block = { analyticsRepository.setWalletBalanceState(userWalletId, balanceState) },
catch = ::raise,
)
}
}

View file

@ -0,0 +1,5 @@
package com.tangem.domain.analytics.model
enum class WalletBalanceState {
ToppedUp, Empty, Error,
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.analytics.repository
import com.tangem.domain.analytics.model.WalletBalanceState
import com.tangem.domain.wallets.models.UserWalletId
interface AnalyticsRepository {
suspend fun checkIsEventSent(eventId: String): Boolean
suspend fun setIsEventSent(eventId: String)
suspend fun getWalletBalanceState(userWalletId: UserWalletId): WalletBalanceState?
suspend fun setWalletBalanceState(userWalletId: UserWalletId, balanceState: WalletBalanceState)
}

View file

@ -66,6 +66,7 @@ dependencies {
implementation(projects.domain.appCurrency.models) implementation(projects.domain.appCurrency.models)
implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models) implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.analytics)
//TODO: Create api/impl modules for onboarding [REDACTED_JIRA] //TODO: Create api/impl modules for onboarding [REDACTED_JIRA]
implementation(projects.features.onboarding) implementation(projects.features.onboarding)

View file

@ -2,6 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.analytics
import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.OneTimeAnalyticsEvent
import com.tangem.domain.wallets.models.UserWalletId
sealed class WalletScreenAnalyticsEvent { sealed class WalletScreenAnalyticsEvent {
@ -11,6 +13,16 @@ sealed class WalletScreenAnalyticsEvent {
error: Throwable? = null, error: Throwable? = null,
) : AnalyticsEvent(category = "Basic", event = event, params = params, error = error) { ) : AnalyticsEvent(category = "Basic", event = event, params = params, error = error) {
class WalletToppedUp(userWalletId: UserWalletId, walletType: AnalyticsParam.WalletType) :
Basic(
event = "Topped up",
params = mapOf(AnalyticsParam.CURRENCY to walletType.value),
),
OneTimeAnalyticsEvent {
override val oneTimeEventId: String = id + userWalletId.stringValue
}
object WalletOpened : Basic(event = "Wallet Opened") object WalletOpened : Basic(event = "Wallet Opened")
class CardWasScanned(source: AnalyticsParam.ScannedFrom) : Basic( class CardWasScanned(source: AnalyticsParam.ScannedFrom) : Basic(

View file

@ -1,13 +1,16 @@
package com.tangem.feature.wallet.presentation.wallet.analytics.utils package com.tangem.feature.wallet.presentation.wallet.analytics.utils
import arrow.core.Either import arrow.core.Either
import com.tangem.common.extensions.isZero import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.analytics.CheckIsWalletToppedUpUseCase
import com.tangem.domain.analytics.model.WalletBalanceState
import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.NetworkGroup
import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import dagger.hilt.android.scopes.ViewModelScoped import dagger.hilt.android.scopes.ViewModelScoped
import java.math.BigDecimal import java.math.BigDecimal
@ -16,13 +19,43 @@ import javax.inject.Inject
@ViewModelScoped @ViewModelScoped
internal class TokenListAnalyticsSender @Inject constructor( internal class TokenListAnalyticsSender @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsEventHandler: AnalyticsEventHandler,
private val checkIsWalletToppedUpUseCase: CheckIsWalletToppedUpUseCase,
) { ) {
fun send(maybeTokenList: Either<TokenListError, TokenList>) { suspend fun send(userWallet: UserWallet, maybeTokenList: Either<TokenListError, TokenList>) {
val tokenList = (maybeTokenList as? Either.Right)?.value ?: return val tokenList = (maybeTokenList as? Either.Right)?.value ?: return
createCardBalanceState(tokenList)?.let { sendBalanceLoadedEventIfNeeded(tokenList)
analyticsEventHandler.send(event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded(balance = it)) sendToppedUpEventIfNeeded(tokenList, userWallet)
}
private fun sendBalanceLoadedEventIfNeeded(tokenList: TokenList) {
createCardBalanceState(tokenList)?.let { balanceState ->
analyticsEventHandler.send(event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded(balanceState))
}
}
private suspend fun sendToppedUpEventIfNeeded(tokenList: TokenList, userWallet: UserWallet) {
val balanceState = tokenList.toWalletBalanceState() ?: return
val isWalletToppedUp = checkIsWalletToppedUpUseCase(userWallet.walletId, balanceState)
.getOrElse { return }
if (isWalletToppedUp) {
val walletType = if (userWallet.isMultiCurrency) {
AnalyticsParam.WalletType.MultiCurrency
} else {
// For single currency wallets with token list, e.g. Noodle
val currency = when (tokenList) {
is TokenList.Empty -> return
is TokenList.GroupedByNetwork -> tokenList.groups.first().currencies.first()
is TokenList.Ungrouped -> tokenList.currencies.first()
}
AnalyticsParam.WalletType.SingleCurrency(currency.currency.name)
}
analyticsEventHandler.send(WalletScreenAnalyticsEvent.Basic.WalletToppedUp(userWallet.walletId, walletType))
} }
} }
@ -30,10 +63,11 @@ internal class TokenListAnalyticsSender @Inject constructor(
return when (val fiatBalance = tokenList.totalFiatBalance) { return when (val fiatBalance = tokenList.totalFiatBalance) {
is TokenList.FiatBalance.Failed -> fiatBalance.toCardBalanceState(tokenList) is TokenList.FiatBalance.Failed -> fiatBalance.toCardBalanceState(tokenList)
is TokenList.FiatBalance.Loaded -> fiatBalance.toCardBalanceState() is TokenList.FiatBalance.Loaded -> fiatBalance.toCardBalanceState()
TokenList.FiatBalance.Loading -> null is TokenList.FiatBalance.Loading -> null
} }
} }
@Suppress("UnusedReceiverParameter")
private fun TokenList.FiatBalance.Failed.toCardBalanceState(tokenList: TokenList): AnalyticsParam.CardBalanceState { private fun TokenList.FiatBalance.Failed.toCardBalanceState(tokenList: TokenList): AnalyticsParam.CardBalanceState {
val currenciesStatuses = when (tokenList) { val currenciesStatuses = when (tokenList) {
is TokenList.Empty -> emptyList() is TokenList.Empty -> emptyList()
@ -50,13 +84,25 @@ internal class TokenListAnalyticsSender @Inject constructor(
} }
} }
private fun TokenList.FiatBalance.Loaded.toCardBalanceState(): AnalyticsParam.CardBalanceState? { private fun TokenList.FiatBalance.Loaded.toCardBalanceState(): AnalyticsParam.CardBalanceState {
return if (amount > BigDecimal.ZERO) { return if (amount > BigDecimal.ZERO) {
AnalyticsParam.CardBalanceState.Full AnalyticsParam.CardBalanceState.Full
} else if (amount.isZero()) {
AnalyticsParam.CardBalanceState.Empty
} else { } else {
null AnalyticsParam.CardBalanceState.Empty
}
}
private fun TokenList.toWalletBalanceState(): WalletBalanceState? {
return when (val balance = totalFiatBalance) {
is TokenList.FiatBalance.Failed -> WalletBalanceState.Error
is TokenList.FiatBalance.Loaded -> {
if (balance.amount > BigDecimal.ZERO) {
WalletBalanceState.ToppedUp
} else {
WalletBalanceState.Empty
}
}
is TokenList.FiatBalance.Loading -> null
} }
} }
} }

View file

@ -38,7 +38,7 @@ internal class SingleWalletWithTokenListSubscriber(
.conflate() .conflate()
.distinctUntilChanged() .distinctUntilChanged()
.onEach(::updateContent) .onEach(::updateContent)
.onEach(tokenListAnalyticsSender::send) .onEach { tokenListAnalyticsSender.send(userWallet, maybeTokenList = it) }
.onEach(walletWithFundsChecker::check) .onEach(walletWithFundsChecker::check)
} }

View file

@ -40,7 +40,7 @@ internal class TokenListSubscriber(
.conflate() .conflate()
.distinctUntilChanged() .distinctUntilChanged()
.onEach(::updateContent) .onEach(::updateContent)
.onEach(tokenListAnalyticsSender::send) .onEach { tokenListAnalyticsSender.send(userWallet, maybeTokenList = it) }
.onEach(walletWithFundsChecker::check) .onEach(walletWithFundsChecker::check)
} }

View file

@ -128,6 +128,7 @@ include(":domain:app-theme:models")
include(":domain:balance-hiding") include(":domain:balance-hiding")
include(":domain:balance-hiding:models") include(":domain:balance-hiding:models")
include(":domain:transaction") include(":domain:transaction")
include(":domain:analytics")
// endregion Domain modules // endregion Domain modules
// region Data modules // region Data modules