Updated on 2026-08-14
This commit is contained in:
parent
a9dfa54eec
commit
9647e6caa3
31 changed files with 302 additions and 40 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
package com.tangem.core.analytics.repository
|
|
||||||
|
|
||||||
interface AnalyticsRepository {
|
|
||||||
|
|
||||||
suspend fun checkIsEventSent(eventId: String): Boolean
|
|
||||||
|
|
||||||
suspend fun setIsEventSent(eventId: String)
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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> */
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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
1
domain/analytics/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
||||||
12
domain/analytics/build.gradle.kts
Normal file
12
domain/analytics/build.gradle.kts
Normal 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)
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.tangem.domain.analytics.model
|
||||||
|
|
||||||
|
enum class WalletBalanceState {
|
||||||
|
ToppedUp, Empty, Error,
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue