Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-04 14:46:26 +04:00
parent 3d5a8654a5
commit 67ca192c16
19 changed files with 90 additions and 52 deletions

View file

@ -3,6 +3,7 @@ package com.tangem.tap.common.analytics.paramsInterceptor
import com.tangem.core.analytics.api.ParamsInterceptor
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.SignIn
import com.tangem.domain.card.analytics.IntroductionProcess
import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
@ -31,6 +32,7 @@ class CardContextInterceptor(
is IntroductionProcess.ButtonScanCardLegacy,
is SignIn.ScreenOpened,
is SignIn.ButtonAddWallet,
is Basic.CardWasScanned,
-> false
is SignIn.ErrorBiometricUpdated -> !event.isFromUnlockAll
else -> true

View file

@ -7,11 +7,8 @@ import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler
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
@ -28,7 +25,6 @@ import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.sdk.extensions.localizedDescriptionRes
import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.mainScope
import com.tangem.tap.tangemSdkManager
@ -61,6 +57,7 @@ internal class LegacyScanProcessor @Inject constructor(
cardId = cardId,
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated,
source = analyticsSource,
)
.doOnFailure { error ->
onScanFailure(analyticsSource = analyticsSource, error = error, onFailure = {}, onCancel = {})
@ -85,10 +82,9 @@ internal class LegacyScanProcessor @Inject constructor(
val result = tangemSdkManager.scanProduct(
cardId = cardId,
shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated,
source = analyticsSource,
)
val analyticsEvent = Basic.CardWasScanned(analyticsSource)
result
.doOnFailure { error ->
scanFailsCounter.onScanFailure(
@ -111,8 +107,6 @@ internal class LegacyScanProcessor @Inject constructor(
scanFailsCounter.reset()
tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)
sendAnalytics(analyticsEvent, scanResponse)
onScanSuccess(
scanResponse = scanResponse,
onProgressStateChange = onProgressStateChange,
@ -122,16 +116,6 @@ internal class LegacyScanProcessor @Inject constructor(
}
}
private fun sendAnalytics(analyticsEvent: AnalyticsEvent, scanResponse: ScanResponse) {
// this workaround needed to send CardWasScannedEvent without adding a context
val interceptor = CardContextInterceptor(scanResponse)
val params = analyticsEvent.params.toMutableMap()
interceptor.intercept(params)
analyticsEvent.params = params.toMap()
Analytics.send(analyticsEvent)
}
private suspend inline fun onScanFailure(
analyticsSource: AnalyticsParam.ScreensSources,
error: TangemError,

View file

@ -18,11 +18,13 @@ internal class DefaultScanCardRepository(
allowRequestAccessCodeFromStorage: Boolean,
shouldCheckIsAlreadyActivated: Boolean,
): ScanResponse {
@Suppress("UnreachableCode")
return when (
val result = tangemSdkManager.scanProduct(
cardId = cardId,
allowsRequestAccessCodeFromRepository = allowRequestAccessCodeFromStorage,
shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated,
source = TODO("Fix when enable NEW_CARD_SCANNING_ENABLED"),
)
) {
is CompletionResult.Success -> result.data

View file

@ -18,6 +18,8 @@ import com.tangem.common.usersCode.UserCodeRepository
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.AnalyticsErrorHandler
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.res.getStringSafe
import com.tangem.crypto.bip39.DefaultMnemonic
import com.tangem.crypto.hdWallet.DerivationPath
@ -25,13 +27,13 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.WithdrawalSignatureResult
import com.tangem.domain.visa.model.*
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DerivationTaskResponse
@ -46,6 +48,7 @@ import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.api.visa.VisaCardActivationResponse
import com.tangem.sdk.api.visa.VisaCardActivationTaskMode
import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent
import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
import com.tangem.tap.domain.tasks.product.*
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
import com.tangem.tap.domain.tasks.visa.TangemPaySignWithdrawalHashTask
@ -135,6 +138,7 @@ internal class DefaultTangemSdkManager(
messageRes: Int?,
allowsRequestAccessCodeFromRepository: Boolean,
shouldCheckIsAlreadyActivated: Boolean,
source: AnalyticsParam.ScreensSources,
): CompletionResult<ScanResponse> {
val message = Message(resources.getStringSafe(messageRes ?: R.string.initial_message_scan_header))
return coroutineScope {
@ -152,7 +156,7 @@ internal class DefaultTangemSdkManager(
),
cardId = cardId,
initialMessage = message,
).also { sendScanResultsToAnalytics(it) }
).also { sendAnalytics(result = it, source = source) }
}
}
@ -225,9 +229,21 @@ internal class DefaultTangemSdkManager(
.doOnResult { tangemSdk.config.setupForProduct(ProductType.ANY) }
}
private fun sendScanResultsToAnalytics(result: CompletionResult<ScanResponse>) {
if (result is CompletionResult.Failure) {
(result.error as? TangemSdkError)?.let { error ->
private fun sendAnalytics(result: CompletionResult<ScanResponse>, source: AnalyticsParam.ScreensSources) {
result
.doOnSuccess { scanResponse ->
// We don't use the standard analytics event enrichment mechanism with card context params here,
// because we've just scanned a new card that may not yet be selected as the current one
val analyticsEvent = Basic.CardWasScanned(source)
val interceptor = CardContextInterceptor(scanResponse)
val params = analyticsEvent.params.toMutableMap()
interceptor.intercept(params)
analyticsEvent.params = params.toMap()
Analytics.send(event = analyticsEvent)
}
.doOnFailure { tangemError ->
(tangemError as? TangemSdkError)?.let { error ->
Analytics.sendErrorEvent(TangemSdkErrorEvent(error))
}
}

View file

@ -14,6 +14,7 @@ import com.tangem.common.core.TangemSdkError
import com.tangem.common.core.UserCodeRequestPolicy
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.services.InMemoryStorage
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.res.getStringSafe
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
@ -63,6 +64,7 @@ class MockTangemSdkManager(
messageRes: Int?,
allowsRequestAccessCodeFromRepository: Boolean,
shouldCheckIsAlreadyActivated: Boolean,
source: AnalyticsParam.ScreensSources,
): CompletionResult<ScanResponse> {
if (!MockProvider.isPreset) {
val activity = foregroundActivityObserver.foregroundActivity

View file

@ -302,11 +302,14 @@ internal class DefaultUserWalletsListRepository(
}
val scanResponse = unlockMethod.scanResponse ?: run {
when (val res = tangemSdkManagerProvider().scanProduct(
val tangemSdkManager = tangemSdkManagerProvider()
val result = tangemSdkManager.scanProduct(
shouldCheckIsAlreadyActivated = false,
)) {
source = unlockMethod.source,
)
when (result) {
is CompletionResult.Failure -> raise(UnlockWalletError.UserCancelled)
is CompletionResult.Success -> res.data
is CompletionResult.Success -> result.data
}
}

View file

@ -5,13 +5,14 @@ plugins {
}
dependencies {
api(deps.kotlin.coroutines)
api(deps.arrow.core)
api(deps.arrow.fx)
api(projects.domain.models)
api(deps.kotlin.coroutines)
implementation(deps.kotlin.serialization)
api(projects.core.analytics.models)
api(projects.domain.models)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit)
testImplementation(deps.test.mockk)

View file

@ -1,6 +1,7 @@
package com.tangem.domain.common.wallets
import arrow.core.Either
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.common.wallets.error.*
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
@ -145,7 +146,10 @@ interface UserWalletsListRepository {
sealed class UnlockMethod {
data object Biometric : UnlockMethod()
data object AccessCode : UnlockMethod()
data class Scan(val scanResponse: ScanResponse? = null) : UnlockMethod()
data class Scan(
val scanResponse: ScanResponse? = null,
val source: AnalyticsParam.ScreensSources,
) : UnlockMethod()
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.domain.wallets.usecase
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.error.UnlockWalletError
import com.tangem.domain.models.wallet.UserWallet
@ -22,7 +23,10 @@ class NonBiometricUnlockWalletUseCase(
private val walletsRepository: WalletsRepository,
) {
suspend operator fun invoke(userWalletId: UserWalletId): Either<UnlockWalletError, Unit> = either {
suspend operator fun invoke(
userWalletId: UserWalletId,
source: AnalyticsParam.ScreensSources,
): Either<UnlockWalletError, Unit> = either {
val userWallet = userWalletsListRepository.userWalletsSync()
.find { it.walletId == userWalletId }
?: raise(UnlockWalletError.UserWalletNotFound)
@ -32,7 +36,7 @@ class NonBiometricUnlockWalletUseCase(
}
val method = when (userWallet) {
is UserWallet.Cold -> UserWalletsListRepository.UnlockMethod.Scan()
is UserWallet.Cold -> UserWalletsListRepository.UnlockMethod.Scan(scanResponse = null, source = source)
is UserWallet.Hot -> UserWalletsListRepository.UnlockMethod.AccessCode
}

View file

@ -2,6 +2,7 @@ package com.tangem.domain.wallets.usecase
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.error.UnlockWalletError
import com.tangem.domain.models.wallet.UserWalletId
@ -19,12 +20,15 @@ class UnlockWalletUseCase(
private val nonBiometricUnlockWalletUseCase: NonBiometricUnlockWalletUseCase,
) {
suspend operator fun invoke(userWalletId: UserWalletId): Either<UnlockWalletError, Unit> = either {
suspend operator fun invoke(
userWalletId: UserWalletId,
source: AnalyticsParam.ScreensSources,
): Either<UnlockWalletError, Unit> = either {
userWalletsListRepository.unlock(userWalletId, UserWalletsListRepository.UnlockMethod.Biometric)
.mapLeft { error ->
when (error) {
UnlockWalletError.AlreadyUnlocked -> Unit
else -> nonBiometricUnlockWalletUseCase(userWalletId).bind()
else -> nonBiometricUnlockWalletUseCase(userWalletId, source).bind()
}
}
}

View file

@ -227,7 +227,10 @@ internal class CreateWalletStartModel @Inject constructor(
is SaveWalletError.WalletAlreadySaved -> {
userWalletsListRepository.unlock(
userWalletId = userWallet.walletId,
unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse),
unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(
scanResponse = scanResponse,
source = AnalyticsParam.ScreensSources.Intro,
),
).onRight {
appRouter.replaceAll(AppRoute.Wallet)
}

View file

@ -421,7 +421,10 @@ internal class CreateWalletStartModelTest {
coVerify {
userWalletsListRepository.unlock(
userWalletId = testUserWalletId,
unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(testScanResponse),
unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(
scanResponse = testScanResponse,
source = AnalyticsParam.ScreensSources.Intro,
),
)
}
verify { appRouter.replaceAll(routes = arrayOf(AppRoute.Wallet), onComplete = any()) }

View file

@ -13,22 +13,22 @@ import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.settings.HotWalletRestrictionManager
import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents
import com.tangem.domain.wallets.usecase.ApplyUserWalletListSortingUseCase
import com.tangem.domain.wallets.usecase.UnlockWalletUseCase
import com.tangem.features.details.entity.UserWalletListUM
import com.tangem.features.details.entity.WalletReorderUM
import com.tangem.features.details.impl.R
import com.tangem.domain.settings.HotWalletRestrictionManager
import com.tangem.features.details.utils.UserWalletSaver
import com.tangem.features.wallet.utils.UserWalletsFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
@Suppress("LongParameterList")
@ -112,7 +112,7 @@ internal class UserWalletListModel @Inject constructor(
private fun onWalletClicked(userWalletId: UserWalletId) {
modelScope.launch {
unlockWalletUseCase(userWalletId)
unlockWalletUseCase(userWalletId, AnalyticsParam.ScreensSources.Settings)
.onRight { router.push(AppRoute.WalletSettings(userWalletId)) }
.onLeft { error ->
TangemLogger.e("Failed to unlock wallet $userWalletId: $error")

View file

@ -29,12 +29,12 @@ import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.features.hotwallet.createhardwarewallet.entity.CreateHardwareWalletUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
private const val HIDE_PROGRESS_DELAY = 400L
@ -181,7 +181,10 @@ internal class CreateHardwareWalletModel @Inject constructor(
)
userWalletsListRepository.unlock(
userWalletId = walletId,
unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse),
unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(
scanResponse = scanResponse,
source = AnalyticsParam.ScreensSources.AddNew,
),
).onRight {
router.replaceAll(AppRoute.Wallet)
}

View file

@ -115,7 +115,10 @@ internal class UpgradeWalletModel @Inject constructor(
)
tangemSdkManager
.scanProduct(shouldCheckIsAlreadyActivated = true)
.scanProduct(
shouldCheckIsAlreadyActivated = true,
source = AnalyticsParam.ScreensSources.Upgrade,
)
.doOnSuccess { scanResponse ->
checkIsWalletSuitableToBeUsedAsUpgrade(scanResponse = scanResponse) {
delay(DELAY_SDK_DIALOG_CLOSE)

View file

@ -1,7 +1,6 @@
package com.tangem.feature.wallet.child.wallet.model.intents
import arrow.core.getOrElse
import com.tangem.utils.logging.TangemLogger
import com.tangem.common.routing.AppRoute.*
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.notifications.NotificationId
@ -14,6 +13,7 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.review.ReviewManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.assetsdiscovery.usecase.AcknowledgeAssetsDiscoveryCompletionUseCase
import com.tangem.domain.card.SetCardWasScannedUseCase
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
@ -41,16 +41,15 @@ import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.Program
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.PromotionBannerClicked
import com.tangem.domain.tokens.model.details.NavigationAction
import com.tangem.domain.assetsdiscovery.usecase.AcknowledgeAssetsDiscoveryCompletionUseCase
import com.tangem.domain.wallets.usecase.*
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
@ -174,7 +173,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
}
override fun onGenerateMissedAddressesClick(missedAddressCurrencies: List<CryptoCurrency>) {
analyticsEventHandler.send(Basic.CardWasScanned(AnalyticsParam.ScreensSources.Main))
analyticsEventHandler.send(MainScreen.NoticeScanYourCardTapped())
modelScope.launch {
@ -199,7 +197,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
userWalletsListRepository.unlockAllWallets()
.onLeft {
val selectedUserWalletId = stateHolder.getSelectedWalletId()
nonBiometricUnlockWalletUseCase(selectedUserWalletId)
nonBiometricUnlockWalletUseCase(selectedUserWalletId, AnalyticsParam.ScreensSources.Main)
.onLeft { error ->
error.handle(
onAlreadyUnlocked = {},

View file

@ -208,8 +208,11 @@ internal class WelcomeModel @Inject constructor(
).onLeft { error ->
if (error is SaveWalletError.WalletAlreadySaved) {
userWalletsListRepository.unlock(
userWallet.walletId,
unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse),
userWalletId = userWallet.walletId,
unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(
scanResponse = scanResponse,
source = AnalyticsParam.ScreensSources.SignIn,
),
).onRight {
userWalletsListRepository.select(userWallet.walletId)
router.replaceAll(AppRoute.Wallet)
@ -274,7 +277,7 @@ internal class WelcomeModel @Inject constructor(
}
private suspend fun nonBiometricUnlockWallet(userWalletId: UserWalletId) {
nonBiometricUnlockWalletUseCase(userWalletId)
nonBiometricUnlockWalletUseCase(userWalletId, AnalyticsParam.ScreensSources.SignIn)
.onRight {
routedOut = true
userWalletsListRepository.select(userWalletId)

View file

@ -14,6 +14,7 @@ dependencies {
implementation(projects.domain.models)
implementation(projects.domain.visa.models)
api(projects.core.analytics.models)
implementation(projects.core.configToggles)
implementation(projects.core.res)

View file

@ -12,6 +12,7 @@ import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.UserCodeRequestPolicy
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.services.secure.SecureStorage
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.scan.CardDTO
@ -57,6 +58,7 @@ interface TangemSdkManager {
messageRes: Int? = null,
allowsRequestAccessCodeFromRepository: Boolean = false,
shouldCheckIsAlreadyActivated: Boolean,
source: AnalyticsParam.ScreensSources,
): CompletionResult<ScanResponse>
suspend fun createProductWallet(