Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-12 13:27:24 +02:00
parent 0fe533f267
commit 7a7204c6c4
32 changed files with 386 additions and 120 deletions

View file

@ -169,6 +169,7 @@ dependencies {
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.navigation)
implementation(projects.core.abTests)
implementation(projects.core.configToggles)
implementation(projects.core.res)
implementation(projects.core.ui)

View file

@ -5,7 +5,9 @@ import com.tangem.TangemSdkLogger
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.routing.AppRouter
import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.core.analytics.filter.OneTimeEventFilter
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.decompose.di.GlobalUiMessageSender
@ -151,4 +153,8 @@ interface ApplicationEntryPoint {
fun getHotWalletFeatureToggles(): HotWalletFeatureToggles
fun getWcInitializeUseCase(): WcInitializeUseCase
fun getTrackingContextProxy(): TrackingContextProxy
fun getABTestsManager(): ABTestsManager
}

View file

@ -16,6 +16,7 @@ import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.routing.AppRouter
import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.ParamsInterceptor
import com.tangem.core.analytics.filter.OneTimeEventFilter
@ -240,6 +241,12 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
private val wcInitializeUseCase
get() = entryPoint.getWcInitializeUseCase()
private val trackingContextProxy
get() = entryPoint.getTrackingContextProxy()
private val abTestsManager: ABTestsManager
get() = entryPoint.getABTestsManager()
// endregion
private val appScope = MainScope()
@ -310,6 +317,8 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize())
}
abTestsManager.init()
appScope.launch {
launch(Dispatchers.IO) {
loadNativeLibraries()
@ -385,6 +394,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
userWalletsListRepository = userWalletsListRepository,
tangemHotSdk = tangemHotSdk,
hotWalletFeatureToggles = hotWalletFeatureToggles,
trackingContextProxy = trackingContextProxy,
),
),
)

View file

@ -1,46 +0,0 @@
package com.tangem.tap.common.analytics
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.utils.AnalyticsContextProxy
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.addHotWalletContext
import com.tangem.tap.common.extensions.eraseContext
import com.tangem.tap.common.extensions.removeContext
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.extensions.setHotWalletContext
/**
[REDACTED_AUTHOR]
*/
internal class DefaultAnalyticsContextProxy : AnalyticsContextProxy {
override fun setContext(scanResponse: ScanResponse) {
Analytics.setContext(scanResponse)
}
override fun addContext(userWallet: UserWallet) {
Analytics.addContext(userWallet)
}
override fun setHotWalletContext() {
Analytics.setHotWalletContext()
}
override fun eraseContext() {
Analytics.eraseContext()
}
override fun addContext(scanResponse: ScanResponse) {
Analytics.addContext(scanResponse)
}
override fun addHotWalletContext() {
Analytics.addHotWalletContext()
}
override fun removeContext() {
Analytics.removeContext()
}
}

View file

@ -1,13 +0,0 @@
package com.tangem.tap.common.analytics
import com.tangem.core.analytics.Analytics
import com.tangem.domain.analytics.ChangeCardAnalyticsContextUseCase
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.setContext
internal class DefaultChangeCardAnalyticsContextUseCase : ChangeCardAnalyticsContextUseCase {
override fun invoke(scanResponse: ScanResponse) {
Analytics.setContext(scanResponse)
}
}

View file

@ -0,0 +1,98 @@
package com.tangem.tap.common.analytics
import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.addHotWalletContext
import com.tangem.tap.common.extensions.eraseContext
import com.tangem.tap.common.extensions.removeContext
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.extensions.setHotWalletContext
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.toHexString
import com.tangem.domain.models.wallet.UserWalletId
/**
[REDACTED_AUTHOR]
*/
internal class DefaultTrackingContextProxy(private val abTestsManager: ABTestsManager) : TrackingContextProxy {
override fun setContext(scanResponse: ScanResponse) {
val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
Analytics.setContext(userWalletId, scanResponse)
abTestsManager.setUserProperties(
userId = calculateUserIdHash(userWalletId),
batch = scanResponse.card.batchId,
productType = scanResponse.productType.name,
firmware = scanResponse.card.firmwareVersion.stringValue,
)
}
override fun setContext(userWallet: UserWallet) {
Analytics.setContext(userWallet)
when (userWallet) {
is UserWallet.Cold -> {
setColdWalletUserProperties(userWallet)
}
is UserWallet.Hot -> {
setHotWalletUserProperties(userWallet)
}
}
}
override fun addContext(userWallet: UserWallet) {
Analytics.addContext(userWallet)
}
override fun setHotWalletContext() {
Analytics.setHotWalletContext()
}
override fun eraseContext() {
Analytics.eraseContext()
abTestsManager.removeUserProperties()
}
override fun addContext(scanResponse: ScanResponse) {
Analytics.addContext(scanResponse)
}
override fun addHotWalletContext() {
Analytics.addHotWalletContext()
}
override fun removeContext() {
Analytics.removeContext()
}
private fun calculateUserIdHash(userWalletId: UserWalletId?): String? {
return userWalletId?.value
?.calculateSha256()
?.toHexString()
}
private fun setColdWalletUserProperties(userWallet: UserWallet.Cold) {
abTestsManager.setUserProperties(
userId = calculateUserIdHash(userWallet.walletId),
batch = userWallet.scanResponse.card.batchId,
productType = userWallet.scanResponse.productType.name,
firmware = userWallet.scanResponse.card.firmwareVersion.stringValue,
)
}
private fun setHotWalletUserProperties(userWallet: UserWallet.Hot) {
abTestsManager.setUserProperties(
userId = calculateUserIdHash(userWallet.walletId),
batch = null,
productType = "Mobile Wallet",
firmware = null,
)
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.tap.common.extensions
import com.tangem.core.analytics.Analytics
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.tap.common.analytics.paramsInterceptor.HotWalletContextInterceptor
import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterceptor
@ -14,8 +15,7 @@ import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterc
/**
* Sets the new context
*/
fun Analytics.setContext(scanResponse: ScanResponse) {
val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
fun Analytics.setContext(userWalletId: UserWalletId?, scanResponse: ScanResponse) {
if (userWalletId != null) {
setUserId(userWalletId.stringValue)
}
@ -63,7 +63,7 @@ fun Analytics.addContext(userWallet: UserWallet) {
is UserWallet.Hot -> HotWalletContextInterceptor(parent = currentContext)
}
setUserId(userWalletId = userWallet.walletId.stringValue)
setUserId(userId = userWallet.walletId.stringValue)
addParamsInterceptor(newContext)
}

View file

@ -1,10 +1,9 @@
package com.tangem.tap.di.analytics
import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.core.analytics.AppInstanceIdProvider
import com.tangem.core.analytics.utils.AnalyticsContextProxy
import com.tangem.domain.analytics.ChangeCardAnalyticsContextUseCase
import com.tangem.tap.common.analytics.DefaultAnalyticsContextProxy
import com.tangem.tap.common.analytics.DefaultChangeCardAnalyticsContextUseCase
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.tap.common.analytics.DefaultTrackingContextProxy
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAppInstanceIdProvider
import dagger.Module
import dagger.Provides
@ -18,13 +17,8 @@ internal object AnalyticsModule {
@Provides
@Singleton
fun provideChangeCardAnalyticsContextUseCase(): ChangeCardAnalyticsContextUseCase {
return DefaultChangeCardAnalyticsContextUseCase()
}
@Provides
@Singleton
fun provideAnalyticsContextProxy(): AnalyticsContextProxy = DefaultAnalyticsContextProxy()
fun provideAnalyticsContextProxy(abtestsManager: ABTestsManager): TrackingContextProxy =
DefaultTrackingContextProxy(abtestsManager)
@Provides
@Singleton

View file

@ -2,11 +2,11 @@ package com.tangem.tap.domain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
@ -34,7 +34,8 @@ class TapWalletManager(
}
private suspend fun loadUserWalletData(userWallet: UserWallet) {
Analytics.setContext(userWallet)
val trackingContextProxy = store.inject(DaggerGraphState::trackingContextProxy)
trackingContextProxy.setContext(userWallet)
if (userWallet is UserWallet.Cold) {
val scanResponse = userWallet.scanResponse

View file

@ -12,6 +12,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.R
@ -47,6 +48,7 @@ import javax.inject.Singleton
internal class LegacyScanProcessor @Inject constructor(
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
private val analyticsEventHandler: AnalyticsEventHandler,
private val trackingContextProxy: TrackingContextProxy,
) {
suspend fun scan(
@ -211,7 +213,7 @@ internal class LegacyScanProcessor @Inject constructor(
},
) {
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
Analytics.addContext(scanResponse)
trackingContextProxy.addContext(scanResponse)
onWalletNotCreated()
navigateTo(
AppRoute.Onboarding(
@ -220,7 +222,7 @@ internal class LegacyScanProcessor @Inject constructor(
),
) { onProgressStateChange(it) }
} else {
Analytics.setContext(scanResponse)
trackingContextProxy.setContext(scanResponse)
val wasTwinsOnboardingShown =
store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync()

View file

@ -3,15 +3,12 @@ package com.tangem.tap.domain.scanCard.chains
import arrow.core.left
import arrow.core.right
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.card.common.util.twinsIsTwinned
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.core.chain.ResultChain
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.proxy.redux.DaggerGraphState
@ -34,9 +31,11 @@ class CheckForOnboardingChain(
) : ResultChain<ScanCardException, ScanResponse>() {
override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult {
val trackingContextProxy = store.inject(DaggerGraphState::trackingContextProxy)
return when {
OnboardingHelper.isOnboardingCase(previousChainResult) -> {
Analytics.addContext(previousChainResult)
trackingContextProxy.addContext(previousChainResult)
ScanChainException.OnboardingNeeded(
AppRoute.Onboarding(
scanResponse = previousChainResult,
@ -45,7 +44,7 @@ class CheckForOnboardingChain(
).left()
}
else -> {
Analytics.setContext(previousChainResult)
trackingContextProxy.setContext(previousChainResult)
val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase)
.invokeSync()

View file

@ -4,9 +4,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.common.keyboard.KeyboardValidator
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.event.TechAnalyticsEvent
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.R
@ -38,7 +38,6 @@ import com.tangem.domain.wallets.usecase.GetSavedWalletsCountUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.domain.wallets.usecase.UpdateRemoteWalletsInfoUseCase
import com.tangem.feature.swap.analytics.StoriesEvents
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.network.exchangeServices.SellService
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.routing.configurator.AppRouterConfig
@ -82,6 +81,7 @@ internal class MainViewModel @Inject constructor(
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
private val appRouterConfig: AppRouterConfig,
private val sellService: SellService,
private val trackingContextProxy: TrackingContextProxy,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel() {
@ -177,7 +177,7 @@ internal class MainViewModel @Inject constructor(
.mapLeft { emptyFlow<UserWallet>() }
.onRight { wallet ->
wallet.distinctUntilChanged()
.onEach { Analytics.setContext(it) }
.onEach { trackingContextProxy.setContext(it) }
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
}

View file

@ -100,7 +100,10 @@ internal class WelcomeMiddleware {
val currency = ParamCardCurrencyConverter().convert(
value = scanResponse.cardTypesResolver,
)
Analytics.addContext(scanResponse)
val trackingContextProxy = store.inject(DaggerGraphState::trackingContextProxy)
trackingContextProxy.addContext(scanResponse)
if (currency != null) {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)

View file

@ -3,6 +3,7 @@ package com.tangem.tap.proxy.redux
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.core.navigation.share.ShareManager
@ -79,4 +80,5 @@ data class DaggerGraphState(
val userWalletsListRepository: UserWalletsListRepository? = null,
val hotWalletFeatureToggles: HotWalletFeatureToggles? = null,
val tangemHotSdk: TangemHotSdk? = null,
val trackingContextProxy: TrackingContextProxy? = null,
) : StateType

1
core/ab-tests/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,31 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
alias(deps.plugins.ksp)
id("configuration")
}
android {
namespace = "com.tangem.core.abtests"
}
dependencies {
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
/** Other libraries */
implementation(deps.timber)
/** Core modules */
implementation(projects.core.analytics.models)
implementation(projects.core.datasource)
implementation(projects.core.utils)
implementation(projects.domain.models)
/** Amplitude experiment */
implementation(deps.amplitude.experiment)
}

View file

@ -0,0 +1,40 @@
package com.tangem.core.abtests.di
import android.app.Application
import com.tangem.core.abtests.BuildConfig
import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.core.abtests.manager.impl.AmplitudeABTestsManager
import com.tangem.core.abtests.manager.impl.StubABTestsManager
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object ABTestsManagerModule {
@Provides
@Singleton
fun provideABTestsManager(
application: Application,
environmentConfigStorage: EnvironmentConfigStorage,
dispatchers: CoroutineDispatcherProvider,
): ABTestsManager {
return if (BuildConfig.AB_TESTS_ENABLED) {
StubABTestsManager()
} else {
AmplitudeABTestsManager(
application = application,
apiKeyProvider = Provider { environmentConfigStorage.getConfigSync().amplitudeApiKey },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
)
}
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.core.abtests.manager
interface ABTestsManager {
fun init()
fun setUserProperties(userId: String?, batch: String?, productType: String?, firmware: String?)
fun removeUserProperties()
fun getValue(key: String, defaultValue: String): String
}

View file

@ -0,0 +1,95 @@
package com.tangem.core.abtests.manager.impl
import android.app.Application
import com.amplitude.experiment.Experiment
import com.amplitude.experiment.ExperimentClient
import com.amplitude.experiment.ExperimentConfig
import com.amplitude.experiment.ExperimentUser
import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.utils.Provider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
internal class AmplitudeABTestsManager(
val application: Application,
val apiKeyProvider: Provider<String>,
val scope: CoroutineScope,
) : ABTestsManager {
private lateinit var client: ExperimentClient
override fun init() {
if (::client.isInitialized) {
Timber.w("AB Tests manager already initialized, skipping")
return
}
client = Experiment.initializeWithAmplitudeAnalytics(
application = application,
apiKey = apiKeyProvider(),
config = ExperimentConfig
.builder()
.automaticFetchOnAmplitudeIdentityChange(true)
.build(),
)
scope.launch {
try {
client.fetch().get()
val allVariants = client.all()
logAllVariants(allVariants)
} catch (exception: Exception) {
Timber.e(exception, "Failed to fetch AB test variants")
}
}
}
override fun setUserProperties(userId: String?, batch: String?, productType: String?, firmware: String?) {
val userProperties = mutableMapOf<String, Any>()
batch?.let { userProperties[AnalyticsParam.BATCH] = it }
productType?.let { userProperties[AnalyticsParam.PRODUCT_TYPE] = it }
firmware?.let { userProperties[AnalyticsParam.FIRMWARE] = it }
client.setUser(
ExperimentUser
.builder()
.userId(userId)
.userProperties(userProperties)
.build(),
)
}
override fun removeUserProperties() {
client.setUser(ExperimentUser())
}
override fun getValue(key: String, defaultValue: String): String {
return client.variant(key).value ?: defaultValue
}
private fun logAllVariants(allVariants: Map<String, com.amplitude.experiment.Variant>) {
Timber.d("=".repeat(SEPARATOR_LENGTH))
Timber.d("AB Tests: Fetched ${allVariants.size} variants")
Timber.d("=".repeat(SEPARATOR_LENGTH))
if (allVariants.isEmpty()) {
Timber.d("No variants available")
} else {
allVariants.entries.forEachIndexed { index, (key, variant) ->
Timber.d("[${index + 1}/${allVariants.size}] Key: $key")
Timber.d(" → Value: ${variant.value ?: "null"}")
Timber.d(" → Payload: ${variant.payload}")
Timber.d(" → Key: ${variant.key}")
Timber.d("-".repeat(SEPARATOR_LENGTH))
}
}
Timber.d("=".repeat(SEPARATOR_LENGTH))
}
private companion object {
const val SEPARATOR_LENGTH = 50
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.core.abtests.manager.impl
import com.tangem.core.abtests.manager.ABTestsManager
internal class StubABTestsManager : ABTestsManager {
override fun init() {
// intentionally do nothing
}
override fun setUserProperties(userId: String?, batch: String?, productType: String?, firmware: String?) {
// intentionally do nothing
}
override fun removeUserProperties() {
// intentionally do nothing
}
override fun getValue(key: String, defaultValue: String): String {
return defaultValue
}
}

View file

@ -61,9 +61,9 @@ object Analytics : GlobalAnalyticsEventHandler {
return paramsInterceptors.remove(interceptorId)
}
override fun setUserId(userWalletId: String) {
override fun setUserId(userId: String) {
analyticsScope.launch {
val userIdHash = userWalletId.hexToBytes()
val userIdHash = userId.hexToBytes()
.calculateSha256()
.toHexString()

View file

@ -6,10 +6,12 @@ import com.tangem.domain.models.wallet.UserWallet
/**
[REDACTED_AUTHOR]
*/
interface AnalyticsContextProxy {
interface TrackingContextProxy {
fun setContext(scanResponse: ScanResponse)
fun setContext(userWallet: UserWallet)
fun addContext(userWallet: UserWallet)
fun setHotWalletContext()

View file

@ -1,8 +0,0 @@
package com.tangem.domain.analytics
import com.tangem.domain.models.scan.ScanResponse
interface ChangeCardAnalyticsContextUseCase {
operator fun invoke(scanResponse: ScanResponse)
}

View file

@ -2,7 +2,7 @@ package com.tangem.features.hotwallet.addexistingwallet.entry
import com.arkivanov.decompose.router.stack.*
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.utils.AnalyticsContextProxy
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
@ -33,7 +33,7 @@ internal class AddExistingWalletModel @Inject constructor(
private val router: Router,
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
private val analyticsContextProxy: AnalyticsContextProxy,
private val trackingContextProxy: TrackingContextProxy,
) : Model() {
val hotWalletStepperComponentModelCallback = HotWalletStepperComponentModelCallback()
@ -48,12 +48,12 @@ internal class AddExistingWalletModel @Inject constructor(
val currentRoute: MutableStateFlow<AddExistingWalletRoute> = MutableStateFlow(startRoute)
init {
analyticsContextProxy.addHotWalletContext()
trackingContextProxy.addHotWalletContext()
}
override fun onDestroy() {
super.onDestroy()
analyticsContextProxy.removeContext()
trackingContextProxy.removeContext()
}
fun onChildBack() {

View file

@ -1,7 +1,7 @@
package com.tangem.features.hotwallet.createmobilewallet
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.utils.AnalyticsContextProxy
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.navigation.Router
@ -26,7 +26,7 @@ internal class CreateMobileWalletModel @Inject constructor(
private val saveUserWalletUseCase: SaveWalletUseCase,
private val router: Router,
private val tangemHotSdk: TangemHotSdk,
private val analyticsContextProxy: AnalyticsContextProxy,
private val trackingContextProxy: TrackingContextProxy,
) : Model() {
internal val uiState: StateFlow<CreateMobileWalletUM>
@ -40,12 +40,12 @@ internal class CreateMobileWalletModel @Inject constructor(
)
init {
analyticsContextProxy.addHotWalletContext()
trackingContextProxy.addHotWalletContext()
}
override fun onDestroy() {
super.onDestroy()
analyticsContextProxy.removeContext()
trackingContextProxy.removeContext()
}
private fun onImportClick() {

View file

@ -4,7 +4,7 @@ import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.push
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.utils.AnalyticsContextProxy
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -26,7 +26,7 @@ internal class CreateWalletBackupModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val analyticsContextProxy: AnalyticsContextProxy,
private val trackingContextProxy: TrackingContextProxy,
) : Model() {
val params = paramsContainer.require<CreateWalletBackupComponent.Params>()
@ -41,12 +41,12 @@ internal class CreateWalletBackupModel @Inject constructor(
val currentRoute: MutableStateFlow<CreateWalletBackupRoute> = MutableStateFlow(startRoute)
init {
analyticsContextProxy.addHotWalletContext()
trackingContextProxy.addHotWalletContext()
}
override fun onDestroy() {
super.onDestroy()
analyticsContextProxy.removeContext()
trackingContextProxy.removeContext()
}
fun onBack() {

View file

@ -4,7 +4,7 @@ import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.push
import com.arkivanov.decompose.router.stack.replaceAll
import com.tangem.core.analytics.utils.AnalyticsContextProxy
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
@ -40,7 +40,7 @@ internal class WalletActivationModel @Inject constructor(
private val router: Router,
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
private val analyticsContextProxy: AnalyticsContextProxy,
private val trackingContextProxy: TrackingContextProxy,
) : Model() {
val params = paramsContainer.require<WalletActivationComponent.Params>()
@ -59,12 +59,12 @@ internal class WalletActivationModel @Inject constructor(
val currentRoute: MutableStateFlow<WalletActivationRoute> = MutableStateFlow(startRoute)
init {
analyticsContextProxy.addHotWalletContext()
trackingContextProxy.addHotWalletContext()
}
override fun onDestroy() {
super.onDestroy()
analyticsContextProxy.removeContext()
trackingContextProxy.removeContext()
}
fun onChildBack() {

View file

@ -10,7 +10,7 @@ import com.tangem.common.routing.AppRoute.ManageTokens.Source
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam.OnOffState.Off
import com.tangem.core.analytics.models.AnalyticsParam.OnOffState.On
import com.tangem.core.analytics.utils.AnalyticsContextProxy
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -70,7 +70,7 @@ internal class WalletSettingsModel @Inject constructor(
private val accountItemsDelegate: AccountItemsDelegate,
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
private val analyticsContextProxy: AnalyticsContextProxy,
private val trackingContextProxy: TrackingContextProxy,
walletCardItemDelegateFactory: WalletCardItemDelegate.Factory,
private val isDemoCardUseCase: IsDemoCardUseCase,
getWalletNFTEnabledUseCase: GetWalletNFTEnabledUseCase,
@ -112,7 +112,7 @@ internal class WalletSettingsModel @Inject constructor(
init {
getUserWalletUseCase.invoke(params.userWalletId).onRight {
analyticsContextProxy.addContext(it)
trackingContextProxy.addContext(it)
}
fun combineUI(wallet: UserWallet) = combine(
@ -160,7 +160,7 @@ internal class WalletSettingsModel @Inject constructor(
override fun onDestroy() {
super.onDestroy()
analyticsContextProxy.removeContext()
trackingContextProxy.removeContext()
}
private fun isNotificationsPermissionGranted(): Boolean {
@ -263,7 +263,7 @@ internal class WalletSettingsModel @Inject constructor(
private fun onLinkMoreCardsClick(scanResponse: ScanResponse) {
analyticsEventHandler.send(Settings.ButtonCreateBackup)
analyticsContextProxy.addContext(scanResponse)
trackingContextProxy.addContext(scanResponse)
router.push(
AppRoute.Onboarding(

View file

@ -48,6 +48,7 @@ compose-lifecycle-runtime = "2.7.0"
# region Other libraries
appsflyer = "6.17.3"
amplitude = "2.36.1"
amplitude-experiment = "1.13.1"
armadillo = "0.9.0"
coil = "2.1.0"
compose-shimmer = "1.0.3"
@ -237,6 +238,7 @@ test-orchestrator = { module = "androidx.test:orchestrator", version.ref = "orch
# region Other
appsflyer = { module = "com.appsflyer:af-android-sdk", version.ref = "appsflyer" }
amplitude = { module = "com.amplitude:android-sdk", version.ref = "amplitude" }
amplitude-experiment = { module = "com.amplitude:experiment-android-client", version.ref = "amplitude-experiment" }
armadillo = { module = "at.favre.lib:armadillo", version.ref = "armadillo" }
coil = { module = "io.coil-kt:coil", version.ref = "coil" }
coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" }

View file

@ -32,4 +32,10 @@ internal sealed class BuildConfigField(val type: String, val name: String, val v
name = "MOCK_DATA_SOURCE",
value = isEnabled.toString(),
)
class ABTestsEnabled(isEnabled: Boolean) : BuildConfigField(
type = "Boolean",
name = "AB_TESTS_ENABLED",
value = isEnabled.toString(),
)
}

View file

@ -28,6 +28,7 @@ internal enum class BuildType(
BuildConfigField.LogEnabled(isEnabled = true),
BuildConfigField.TesterMenuAvailability(isEnabled = true),
BuildConfigField.MockDataSource(isEnabled = false),
BuildConfigField.ABTestsEnabled(isEnabled = false),
),
),
@ -49,6 +50,7 @@ internal enum class BuildType(
BuildConfigField.LogEnabled(isEnabled = true),
BuildConfigField.TesterMenuAvailability(isEnabled = true),
BuildConfigField.MockDataSource(isEnabled = true),
BuildConfigField.ABTestsEnabled(isEnabled = false),
),
),
@ -72,11 +74,12 @@ internal enum class BuildType(
BuildConfigField.LogEnabled(isEnabled = true),
BuildConfigField.TesterMenuAvailability(isEnabled = true),
BuildConfigField.MockDataSource(isEnabled = false),
BuildConfigField.ABTestsEnabled(isEnabled = true),
),
),
/**
* Build type for QA and business
* Build type for support with possible bugfixes for certain cases
*
* Features:
* - Env: prod
@ -92,6 +95,7 @@ internal enum class BuildType(
BuildConfigField.LogEnabled(isEnabled = false),
BuildConfigField.TesterMenuAvailability(isEnabled = false),
BuildConfigField.MockDataSource(isEnabled = false),
BuildConfigField.ABTestsEnabled(isEnabled = false),
),
),
@ -110,6 +114,7 @@ internal enum class BuildType(
BuildConfigField.LogEnabled(isEnabled = false),
BuildConfigField.TesterMenuAvailability(isEnabled = false),
BuildConfigField.MockDataSource(isEnabled = false),
BuildConfigField.ABTestsEnabled(isEnabled = true),
),
),
}

View file

@ -154,6 +154,7 @@ include(":core:analytics")
include(":core:analytics:models")
include(":core:datasource")
include(":core:config-toggles")
include(":core:ab-tests")
include(":core:navigation")
include(":core:res")
include(":core:ui")