Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-03 18:03:15 +05:00
parent ba572cf71f
commit 77683181c7
20 changed files with 189 additions and 145 deletions

View file

@ -113,6 +113,7 @@ dependencies {
implementation(projects.domain.account) implementation(projects.domain.account)
implementation(projects.domain.account.status) implementation(projects.domain.account.status)
implementation(projects.domain.addressBook) implementation(projects.domain.addressBook)
implementation(projects.domain.appsflyer)
implementation(projects.domain.models) implementation(projects.domain.models)
implementation(projects.domain.core) implementation(projects.domain.core)
api(projects.domain.common) api(projects.domain.common)

View file

@ -4,13 +4,10 @@ import com.appsflyer.deeplink.DeepLink
import com.tangem.datasource.local.appsflyer.AppsFlyerDeeplinkSource import com.tangem.datasource.local.appsflyer.AppsFlyerDeeplinkSource
import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.domain.wallets.models.AppsFlyerConversionData import com.tangem.domain.wallets.models.AppsFlyerConversionData
import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase
import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.logging.TangemLogger import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.contracts.ExperimentalContracts import kotlin.contracts.ExperimentalContracts
@ -19,11 +16,9 @@ import kotlin.contracts.contract
@Singleton @Singleton
class AppsFlyerReferralParamsHandler @Inject constructor( class AppsFlyerReferralParamsHandler @Inject constructor(
private val appsFlyerStore: AppsFlyerStore, private val appsFlyerStore: AppsFlyerStore,
private val setShouldShowMobileWalletPromoUseCase: SetShouldShowMobileWalletPromoUseCase,
private val coroutineScope: AppCoroutineScope, private val coroutineScope: AppCoroutineScope,
) { ) {
private val mutex = Mutex()
private val deepLinkDeferred = CompletableDeferred<String?>() private val deepLinkDeferred = CompletableDeferred<String?>()
fun handle(params: Map<String?, Any?>) { fun handle(params: Map<String?, Any?>) {
@ -48,15 +43,17 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
} }
suspend fun waitForDeeplink(deeplinkSource: AppsFlyerDeeplinkSource): String? { suspend fun waitForDeeplink(deeplinkSource: AppsFlyerDeeplinkSource): String? {
val deeplinkFromCache = appsFlyerStore.getDeeplink(deeplinkSource) appsFlyerStore.getDeeplink(deeplinkSource)?.let { return it }
return if (deeplinkFromCache == null) {
val value = when (deeplinkSource) { val expectedValue = when (deeplinkSource) {
AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding -> TANGEM_PAY_HOT_WALLET_ONBOARDING_DEEP_LINK_VALUE AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding -> TANGEM_PAY_HOT_WALLET_ONBOARDING_DEEP_LINK_VALUE
} AppsFlyerDeeplinkSource.Referral -> REFERRAL_DEEP_LINK_VALUE
deepLinkDeferred.await().takeIf { it == value }
} else {
deeplinkFromCache
} }
val resolvedValue = deepLinkDeferred.await().takeIf { it == expectedValue }
// The deep link may have been persisted to the store while we were awaiting (e.g. from
// conversion-data handling, which stores the deep link but doesn't complete the deferred).
return resolvedValue ?: appsFlyerStore.getDeeplink(deeplinkSource)
} }
private fun handle(deepLinkValue: String?, deepLinkSub1: String?, deepLinkSub2: String?) { private fun handle(deepLinkValue: String?, deepLinkSub1: String?, deepLinkSub2: String?) {
@ -79,6 +76,10 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
@Suppress("NullableToStringCall") @Suppress("NullableToStringCall")
TangemLogger.i("refcode=$deepLinkSub1\ncampaign=$deepLinkSub2") TangemLogger.i("refcode=$deepLinkSub1\ncampaign=$deepLinkSub2")
coroutineScope.launch {
appsFlyerStore.storeDeeplink(AppsFlyerDeeplinkSource.Referral, REFERRAL_DEEP_LINK_VALUE)
}
if (!isValidParam(deepLinkSub1)) { if (!isValidParam(deepLinkSub1)) {
TangemLogger.e("Deeplink conversion data is invalid") TangemLogger.e("Deeplink conversion data is invalid")
return return
@ -98,13 +99,9 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
private fun storeConversionData(refcode: String, campaign: String?) { private fun storeConversionData(refcode: String, campaign: String?) {
coroutineScope.launch { coroutineScope.launch {
mutex.withLock { appsFlyerStore.storeIfAbsent(
setShouldShowMobileWalletPromoUseCase(true) value = AppsFlyerConversionData(refcode = refcode, campaign = campaign),
.onLeft { TangemLogger.e("Error", it) } )
appsFlyerStore.storeIfAbsent(
value = AppsFlyerConversionData(refcode = refcode, campaign = campaign),
)
}
} }
} }

View file

@ -9,6 +9,7 @@ import com.tangem.common.services.secure.SecureStorage
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.appsflyer.usecase.ClearAppsFlyerDeeplinkUseCase
import com.tangem.domain.common.wallets.UserWalletSelectedHandler import com.tangem.domain.common.wallets.UserWalletSelectedHandler
import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.hotwallet.repository.HotWalletRepository import com.tangem.domain.hotwallet.repository.HotWalletRepository
@ -17,7 +18,6 @@ import com.tangem.domain.visa.model.VisaActivationRemoteState
import com.tangem.domain.visa.model.VisaCardActivationStatus import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.feature.referral.domain.MobileWalletPromoRepository
import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.sdk.storage.AndroidSecureStorage import com.tangem.sdk.storage.AndroidSecureStorage
import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.sdk.storage.AndroidSecureStorageV2
@ -57,7 +57,7 @@ internal object UserWalletsListRepositoryModule {
trackingContextProxy: TrackingContextProxy, trackingContextProxy: TrackingContextProxy,
analyticsEventHandler: AnalyticsEventHandler, analyticsEventHandler: AnalyticsEventHandler,
hotWalletRepository: HotWalletRepository, hotWalletRepository: HotWalletRepository,
mobileWalletPromoRepository: MobileWalletPromoRepository, clearAppsFlyerDeeplinkUseCase: ClearAppsFlyerDeeplinkUseCase,
userWalletSelectedHandler: Lazy<UserWalletSelectedHandler>, userWalletSelectedHandler: Lazy<UserWalletSelectedHandler>,
): UserWalletsListRepository { ): UserWalletsListRepository {
val moshi = buildMoshi() val moshi = buildMoshi()
@ -109,7 +109,7 @@ internal object UserWalletsListRepositoryModule {
trackingContextProxy = trackingContextProxy, trackingContextProxy = trackingContextProxy,
analyticsEventHandler = analyticsEventHandler, analyticsEventHandler = analyticsEventHandler,
hotWalletRepository = hotWalletRepository, hotWalletRepository = hotWalletRepository,
mobileWalletPromoRepository = mobileWalletPromoRepository, clearAppsFlyerDeeplinkUseCase = clearAppsFlyerDeeplinkUseCase,
userWalletSelectedHandler = userWalletSelectedHandler, userWalletSelectedHandler = userWalletSelectedHandler,
) )
} }

View file

@ -14,6 +14,8 @@ import com.tangem.core.analytics.utils.TrackingContextProxy
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.getSyncOrDefault import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.domain.appsflyer.AppsFlyerDeeplinkSource
import com.tangem.domain.appsflyer.usecase.ClearAppsFlyerDeeplinkUseCase
import com.tangem.domain.common.wallets.UserWalletSelectedHandler import com.tangem.domain.common.wallets.UserWalletSelectedHandler
import com.tangem.domain.common.wallets.UserWalletTransformAction import com.tangem.domain.common.wallets.UserWalletTransformAction
import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.UserWalletsListRepository
@ -26,7 +28,6 @@ import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents
import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.feature.referral.domain.MobileWalletPromoRepository
import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.model.HotWalletId import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.TangemSdkManager
@ -60,7 +61,7 @@ internal class DefaultUserWalletsListRepository(
private val trackingContextProxy: TrackingContextProxy, private val trackingContextProxy: TrackingContextProxy,
private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsEventHandler: AnalyticsEventHandler,
private val hotWalletRepository: HotWalletRepository, private val hotWalletRepository: HotWalletRepository,
private val mobileWalletPromoRepository: MobileWalletPromoRepository, private val clearAppsFlyerDeeplinkUseCase: ClearAppsFlyerDeeplinkUseCase,
private val userWalletSelectedHandler: Lazy<UserWalletSelectedHandler>, private val userWalletSelectedHandler: Lazy<UserWalletSelectedHandler>,
) : UserWalletsListRepository { ) : UserWalletsListRepository {
@ -620,12 +621,12 @@ internal class DefaultUserWalletsListRepository(
} }
private suspend fun onFirstWalletCreated() { private suspend fun onFirstWalletCreated() {
// reset flag (that is set from AF deeplink) after creating a new wallet // reset the referral attribution (set from AF deeplink) after creating a new wallet
mobileWalletPromoRepository.setShouldShowMobileWalletPromo(false) clearAppsFlyerDeeplinkUseCase(AppsFlyerDeeplinkSource.Referral)
} }
private suspend fun onAllWalletsDeleted() { private suspend fun onAllWalletsDeleted() {
// reset flag (that is set from AF deeplink) after removing the last wallet // reset the referral attribution (set from AF deeplink) after removing the last wallet
mobileWalletPromoRepository.setShouldShowMobileWalletPromo(false) clearAppsFlyerDeeplinkUseCase(AppsFlyerDeeplinkSource.Referral)
} }
} }

View file

@ -43,7 +43,6 @@ import com.tangem.domain.onboarding.repository.OnboardingRepository
import com.tangem.domain.settings.NeverRequestPermissionUseCase import com.tangem.domain.settings.NeverRequestPermissionUseCase
import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase
import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase
import com.tangem.feature.referral.domain.ShouldShowMobileWalletPromoUseCase
import com.tangem.features.hotwallet.HotAccessCodeRequestComponent import com.tangem.features.hotwallet.HotAccessCodeRequestComponent
import com.tangem.features.hotwallet.accesscoderequest.proxy.HotWalletPasswordRequesterProxy import com.tangem.features.hotwallet.accesscoderequest.proxy.HotWalletPasswordRequesterProxy
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
@ -69,6 +68,8 @@ import com.tangem.wallet.R
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@ -102,7 +103,6 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
private val shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase, private val shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase,
private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase, private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase,
private val featureTogglesManager: FeatureTogglesManager, private val featureTogglesManager: FeatureTogglesManager,
private val shouldShowMobileWalletPromoUseCase: ShouldShowMobileWalletPromoUseCase,
) : RoutingComponent, ) : RoutingComponent,
AppComponentContext by context, AppComponentContext by context,
SnackbarHandler { SnackbarHandler {
@ -210,23 +210,8 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
} }
private suspend fun navigateForEmptyWallets(): AppRoute { private suspend fun navigateForEmptyWallets(): AppRoute {
val isHotWalletOnboardingEnabled = featureTogglesManager.isFeatureEnabled( val afterEmptyRoute = resolveAppsFlyerOnboardingRoute()
FeatureToggles.AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING, ?: AppRoute.Home(launchMode = launchMode)
)
TangemLogger.i("[TangemPay][HWO] Feature toggle enabled=$isHotWalletOnboardingEnabled")
val afterEmptyRoute: AppRoute = if (isHotWalletOnboardingEnabled) {
val tangemPayHotWalletOnboardingDeepLink = withTimeoutOrNull(2.seconds) {
appsFlyerReferralParamsHandler.waitForDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding)
}
TangemLogger.i("[TangemPay][HWO] Deep link present=${tangemPayHotWalletOnboardingDeepLink != null}")
if (tangemPayHotWalletOnboardingDeepLink != null) {
AppRoute.TangemPayHotWalletOnboarding
} else {
getDefaultRoute()
}
} else {
getDefaultRoute()
}
val shouldAskPushPermission = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrNull() val shouldAskPushPermission = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrNull()
?: return afterEmptyRoute ?: return afterEmptyRoute
@ -246,16 +231,41 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
} }
} }
private suspend fun getDefaultRoute(): AppRoute { private suspend fun resolveAppsFlyerOnboardingRoute(): AppRoute? = coroutineScope {
val isHideStoriesForReferralEnabled = featureTogglesManager.isFeatureEnabled( val tangemPayRoute = async { resolveTangemPayHotWalletOnboardingRoute() }
val referralRoute = async { resolveReferralRoute() }
tangemPayRoute.await() ?: referralRoute.await()
}
private suspend fun resolveTangemPayHotWalletOnboardingRoute(): AppRoute? {
val isEnabled = featureTogglesManager.isFeatureEnabled(
FeatureToggles.AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING,
)
TangemLogger.i("[TangemPay][HWO] Feature toggle enabled=$isEnabled")
if (!isEnabled) return null
val deepLink = awaitAppsFlyerDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding)
TangemLogger.i("[TangemPay][HWO] Deep link present=${deepLink != null}")
return if (deepLink != null) AppRoute.TangemPayHotWalletOnboarding else null
}
private suspend fun resolveReferralRoute(): AppRoute? {
val isEnabled = featureTogglesManager.isFeatureEnabled(
FeatureToggles.TWI_1512_HIDE_STORIES_FOR_REFERRAL_ENABLED, FeatureToggles.TWI_1512_HIDE_STORIES_FOR_REFERRAL_ENABLED,
) )
// Referral users skip the Home stories screen and land directly on the if (!isEnabled) return null
// mobile wallet creation flow.
return if (isHideStoriesForReferralEnabled && shouldShowMobileWalletPromoUseCase()) { val referralDeepLink = awaitAppsFlyerDeeplink(AppsFlyerDeeplinkSource.Referral)
return if (referralDeepLink != null) {
AppRoute.CreateWalletStart(mode = AppRoute.CreateWalletStart.Mode.HotWallet) AppRoute.CreateWalletStart(mode = AppRoute.CreateWalletStart.Mode.HotWallet)
} else { } else {
AppRoute.Home(launchMode = launchMode) null
}
}
private suspend fun awaitAppsFlyerDeeplink(source: AppsFlyerDeeplinkSource): String? {
return withTimeoutOrNull(2.seconds) {
appsFlyerReferralParamsHandler.waitForDeeplink(source)
} }
} }

View file

@ -5,9 +5,7 @@ import com.google.common.truth.Truth.assertThat
import com.tangem.datasource.local.appsflyer.AppsFlyerDeeplinkSource import com.tangem.datasource.local.appsflyer.AppsFlyerDeeplinkSource
import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.domain.wallets.models.AppsFlyerConversionData import com.tangem.domain.wallets.models.AppsFlyerConversionData
import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase
import com.tangem.test.core.ProvideTestModels import com.tangem.test.core.ProvideTestModels
import arrow.core.right
import com.tangem.test.core.TestAppCoroutineScope import com.tangem.test.core.TestAppCoroutineScope
import io.mockk.clearMocks import io.mockk.clearMocks
import io.mockk.coEvery import io.mockk.coEvery
@ -28,13 +26,9 @@ import org.junit.jupiter.params.ParameterizedTest
class AppsFlyerReferralParamsHandlerTest { class AppsFlyerReferralParamsHandlerTest {
private val appsFlyerStore: AppsFlyerStore = mockk(relaxUnitFun = true) private val appsFlyerStore: AppsFlyerStore = mockk(relaxUnitFun = true)
private val setShouldShowMobileWalletPromoUseCase: SetShouldShowMobileWalletPromoUseCase = mockk {
coEvery { this@mockk.invoke(true) } returns Unit.right()
}
private val handler = AppsFlyerReferralParamsHandler( private val handler = AppsFlyerReferralParamsHandler(
appsFlyerStore = appsFlyerStore, appsFlyerStore = appsFlyerStore,
coroutineScope = TestAppCoroutineScope(), coroutineScope = TestAppCoroutineScope(),
setShouldShowMobileWalletPromoUseCase = setShouldShowMobileWalletPromoUseCase,
) )
@AfterEach @AfterEach
@ -175,7 +169,6 @@ class AppsFlyerReferralParamsHandlerTest {
private val localHandler = AppsFlyerReferralParamsHandler( private val localHandler = AppsFlyerReferralParamsHandler(
appsFlyerStore = localStore, appsFlyerStore = localStore,
coroutineScope = TestAppCoroutineScope(), coroutineScope = TestAppCoroutineScope(),
setShouldShowMobileWalletPromoUseCase = mockk { coEvery { this@mockk.invoke(true) } returns Unit.right() },
) )
@Test @Test
@ -240,6 +233,93 @@ class AppsFlyerReferralParamsHandlerTest {
} }
} }
@Nested
inner class WaitForReferralDeeplink {
private val localStore: AppsFlyerStore = mockk(relaxUnitFun = true)
private val localHandler = AppsFlyerReferralParamsHandler(
appsFlyerStore = localStore,
coroutineScope = TestAppCoroutineScope(),
)
@Test
fun `GIVEN cached referral deeplink WHEN waitForDeeplink THEN returns cached value`() = runTest {
// GIVEN
coEvery { localStore.getDeeplink(AppsFlyerDeeplinkSource.Referral) } returns "referral"
// WHEN
val result = localHandler.waitForDeeplink(AppsFlyerDeeplinkSource.Referral)
// THEN
assertThat(result).isEqualTo("referral")
}
@Test
fun `GIVEN no cache and referral deeplink WHEN handleDeeplink then waitForDeeplink THEN returns referral value`() =
runTest {
// GIVEN
coEvery { localStore.getDeeplink(AppsFlyerDeeplinkSource.Referral) } returns null
val deepLink = mockk<DeepLink> {
every { deepLinkValue } returns "referral"
every { getStringValue(any()) } returns SUCCESS_REFCODE
}
// WHEN
localHandler.handleDeeplink(deepLink)
val result = localHandler.waitForDeeplink(AppsFlyerDeeplinkSource.Referral)
// THEN
assertThat(result).isEqualTo("referral")
coVerify { localStore.storeDeeplink(AppsFlyerDeeplinkSource.Referral, "referral") }
}
@Test
fun `GIVEN no cache and non-referral deeplink WHEN handleDeeplink then waitForDeeplink THEN returns null`() =
runTest {
// GIVEN
coEvery { localStore.getDeeplink(AppsFlyerDeeplinkSource.Referral) } returns null
val deepLink = mockk<DeepLink> {
every { deepLinkValue } returns "tpay_mobileonboard"
every { getStringValue(any()) } returns null
}
// WHEN
localHandler.handleDeeplink(deepLink)
val result = localHandler.waitForDeeplink(AppsFlyerDeeplinkSource.Referral)
// THEN
assertThat(result).isNull()
coVerify(inverse = true) { localStore.storeDeeplink(AppsFlyerDeeplinkSource.Referral, any()) }
}
@Test
fun `GIVEN no cache WHEN handleNoDeeplink then waitForDeeplink THEN returns null`() = runTest {
// GIVEN
coEvery { localStore.getDeeplink(AppsFlyerDeeplinkSource.Referral) } returns null
// WHEN
localHandler.handleNoDeeplink()
val result = localHandler.waitForDeeplink(AppsFlyerDeeplinkSource.Referral)
// THEN
assertThat(result).isNull()
}
@Test
fun `GIVEN deeplink stored during wait WHEN waitForDeeplink THEN returns stored value`() = runTest {
// GIVEN cache is empty on the initial read but populated (e.g. from conversion-data
// handling) by the time we re-check after awaiting the deferred
coEvery { localStore.getDeeplink(AppsFlyerDeeplinkSource.Referral) } returns null andThen "referral"
// WHEN the deferred resolves without a matching value (UDL reported no deep link)
localHandler.handleNoDeeplink()
val result = localHandler.waitForDeeplink(AppsFlyerDeeplinkSource.Referral)
// THEN the value persisted during the wait is preferred
assertThat(result).isEqualTo("referral")
}
}
private companion object Companion { private companion object Companion {
const val SUCCESS_REFCODE = "valid_refcode" const val SUCCESS_REFCODE = "valid_refcode"
const val SUCCESS_CAMPAIGN = "valid_campaign" const val SUCCESS_CAMPAIGN = "valid_campaign"

View file

@ -7,13 +7,13 @@ import com.tangem.common.test.domain.wallet.MockUserWalletFactory
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.appsflyer.usecase.ClearAppsFlyerDeeplinkUseCase
import com.tangem.domain.common.wallets.UserWalletSelectedHandler import com.tangem.domain.common.wallets.UserWalletSelectedHandler
import com.tangem.domain.hotwallet.repository.HotWalletRepository import com.tangem.domain.hotwallet.repository.HotWalletRepository
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.feature.referral.domain.MobileWalletPromoRepository
import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.Provider import com.tangem.utils.Provider
@ -45,7 +45,7 @@ internal class DefaultUserWalletsListRepositoryTest {
private val trackingContextProxy: TrackingContextProxy = mockk(relaxed = true) private val trackingContextProxy: TrackingContextProxy = mockk(relaxed = true)
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true) private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
private val hotWalletRepository: HotWalletRepository = mockk(relaxed = true) private val hotWalletRepository: HotWalletRepository = mockk(relaxed = true)
private val mobileWalletPromoRepository: MobileWalletPromoRepository = mockk(relaxed = true) private val clearAppsFlyerDeeplinkUseCase: ClearAppsFlyerDeeplinkUseCase = mockk(relaxed = true)
private val userWalletSelectedHandler: UserWalletSelectedHandler = mockk(relaxed = true) private val userWalletSelectedHandler: UserWalletSelectedHandler = mockk(relaxed = true)
private val walletA = MockUserWalletFactory.create().copy(walletId = UserWalletId("0011"), name = "Wallet A") private val walletA = MockUserWalletFactory.create().copy(walletId = UserWalletId("0011"), name = "Wallet A")
@ -61,7 +61,7 @@ internal class DefaultUserWalletsListRepositoryTest {
selectedUserWalletRepository, selectedUserWalletRepository,
userWalletEncryptionKeysRepository, userWalletEncryptionKeysRepository,
trackingContextProxy, trackingContextProxy,
mobileWalletPromoRepository, clearAppsFlyerDeeplinkUseCase,
userWalletSelectedHandler, userWalletSelectedHandler,
) )
@ -82,7 +82,7 @@ internal class DefaultUserWalletsListRepositoryTest {
trackingContextProxy = trackingContextProxy, trackingContextProxy = trackingContextProxy,
analyticsEventHandler = analyticsEventHandler, analyticsEventHandler = analyticsEventHandler,
hotWalletRepository = hotWalletRepository, hotWalletRepository = hotWalletRepository,
mobileWalletPromoRepository = mobileWalletPromoRepository, clearAppsFlyerDeeplinkUseCase = clearAppsFlyerDeeplinkUseCase,
userWalletSelectedHandler = Lazy { userWalletSelectedHandler }, userWalletSelectedHandler = Lazy { userWalletSelectedHandler },
) )
} }

View file

@ -23,9 +23,11 @@ interface AppsFlyerStore {
enum class AppsFlyerDeeplinkSource { enum class AppsFlyerDeeplinkSource {
TangemPayHotWalletOnboarding, TangemPayHotWalletOnboarding,
Referral,
; ;
fun toStoreKey() = when (this) { fun toStoreKey() = when (this) {
TangemPayHotWalletOnboarding -> "tangem_pay_hot_wallet_onboarding" TangemPayHotWalletOnboarding -> "tangem_pay_hot_wallet_onboarding"
Referral -> "referral"
} }
} }

View file

@ -10,11 +10,16 @@ internal class DefaultAppsFlyerRepository @Inject constructor(
private val appsFlyerStore: AppsFlyerStore, private val appsFlyerStore: AppsFlyerStore,
) : AppsFlyerRepository { ) : AppsFlyerRepository {
override suspend fun getDeeplink(source: AppsFlyerDeeplinkSource): String? {
return appsFlyerStore.getDeeplink(source.toStoreSource())
}
override suspend fun clearDeeplink(source: AppsFlyerDeeplinkSource) { override suspend fun clearDeeplink(source: AppsFlyerDeeplinkSource) {
appsFlyerStore.clearDeeplink(source.toStoreSource()) appsFlyerStore.clearDeeplink(source.toStoreSource())
} }
private fun AppsFlyerDeeplinkSource.toStoreSource() = when (this) { private fun AppsFlyerDeeplinkSource.toStoreSource() = when (this) {
AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding -> StoreDeeplinkSource.TangemPayHotWalletOnboarding AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding -> StoreDeeplinkSource.TangemPayHotWalletOnboarding
AppsFlyerDeeplinkSource.Referral -> StoreDeeplinkSource.Referral
} }
} }

View file

@ -3,6 +3,7 @@ package com.tangem.data.appsflyer.di
import com.tangem.data.appsflyer.DefaultAppsFlyerRepository import com.tangem.data.appsflyer.DefaultAppsFlyerRepository
import com.tangem.domain.appsflyer.repository.AppsFlyerRepository import com.tangem.domain.appsflyer.repository.AppsFlyerRepository
import com.tangem.domain.appsflyer.usecase.ClearAppsFlyerDeeplinkUseCase import com.tangem.domain.appsflyer.usecase.ClearAppsFlyerDeeplinkUseCase
import com.tangem.domain.appsflyer.usecase.IsReferralInstallUseCase
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -26,5 +27,10 @@ internal interface AppsFlyerDataModule {
): ClearAppsFlyerDeeplinkUseCase { ): ClearAppsFlyerDeeplinkUseCase {
return ClearAppsFlyerDeeplinkUseCase(appsFlyerRepository) return ClearAppsFlyerDeeplinkUseCase(appsFlyerRepository)
} }
@Provides
fun provideIsReferralInstallUseCase(appsFlyerRepository: AppsFlyerRepository): IsReferralInstallUseCase {
return IsReferralInstallUseCase(appsFlyerRepository)
}
} }
} }

View file

@ -2,4 +2,5 @@ package com.tangem.domain.appsflyer
enum class AppsFlyerDeeplinkSource { enum class AppsFlyerDeeplinkSource {
TangemPayHotWalletOnboarding, TangemPayHotWalletOnboarding,
Referral,
} }

View file

@ -4,5 +4,7 @@ import com.tangem.domain.appsflyer.AppsFlyerDeeplinkSource
interface AppsFlyerRepository { interface AppsFlyerRepository {
suspend fun getDeeplink(source: AppsFlyerDeeplinkSource): String?
suspend fun clearDeeplink(source: AppsFlyerDeeplinkSource) suspend fun clearDeeplink(source: AppsFlyerDeeplinkSource)
} }

View file

@ -0,0 +1,15 @@
package com.tangem.domain.appsflyer.usecase
import com.tangem.domain.appsflyer.AppsFlyerDeeplinkSource
import com.tangem.domain.appsflyer.repository.AppsFlyerRepository
/**
* Returns whether the app install was attributed to an AppsFlyer referral deep link.
*/
class IsReferralInstallUseCase(
private val appsFlyerRepository: AppsFlyerRepository,
) {
suspend operator fun invoke(): Boolean {
return appsFlyerRepository.getDeeplink(AppsFlyerDeeplinkSource.Referral) != null
}
}

View file

@ -28,6 +28,7 @@ dependencies {
implementation(projects.common.routing) implementation(projects.common.routing)
/** Domain */ /** Domain */
implementation(projects.domain.appsflyer)
implementation(projects.domain.common) implementation(projects.domain.common)
implementation(projects.domain.models) implementation(projects.domain.models)
implementation(projects.domain.core) implementation(projects.domain.core)
@ -41,9 +42,6 @@ dependencies {
implementation(projects.domain.feedback.models) implementation(projects.domain.feedback.models)
implementation(projects.domain.referral) implementation(projects.domain.referral)
/** Referral */
implementation(projects.features.referral.domain)
/** AndroidX libraries */ /** AndroidX libraries */
implementation(deps.androidx.activity.compose) implementation(deps.androidx.activity.compose)
implementation(deps.lifecycle.runtime.ktx) implementation(deps.lifecycle.runtime.ktx)

View file

@ -18,6 +18,7 @@ import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.message.dialog.Dialogs import com.tangem.core.ui.message.dialog.Dialogs
import com.tangem.domain.appsflyer.usecase.IsReferralInstallUseCase
import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.analytics.IntroductionProcess import com.tangem.domain.card.analytics.IntroductionProcess
import com.tangem.domain.card.analytics.Shop import com.tangem.domain.card.analytics.Shop
@ -32,7 +33,6 @@ import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.feature.referral.domain.ShouldShowMobileWalletPromoUseCase
import com.tangem.features.home.api.HomeComponent import com.tangem.features.home.api.HomeComponent
import com.tangem.features.home.impl.ui.state.HomeUM import com.tangem.features.home.impl.ui.state.HomeUM
import com.tangem.features.home.impl.ui.state.Stories import com.tangem.features.home.impl.ui.state.Stories
@ -66,7 +66,7 @@ internal class HomeModel @Inject constructor(
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
private val urlOpener: UrlOpener, private val urlOpener: UrlOpener,
private val userWalletsListRepository: UserWalletsListRepository, private val userWalletsListRepository: UserWalletsListRepository,
private val shouldShowMobileWalletPromoUseCase: ShouldShowMobileWalletPromoUseCase, private val isReferralInstallUseCase: IsReferralInstallUseCase,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
) : Model() { ) : Model() {
@ -136,7 +136,7 @@ internal class HomeModel @Inject constructor(
private fun onGetStartedClick() { private fun onGetStartedClick() {
debouncer.debounce(modelScope) { debouncer.debounce(modelScope) {
modelScope.launch { modelScope.launch {
val mode = if (shouldShowMobileWalletPromoUseCase()) { val mode = if (isReferralInstallUseCase()) {
AppRoute.CreateWalletStart.Mode.HotWallet AppRoute.CreateWalletStart.Mode.HotWallet
} else { } else {
AppRoute.CreateWalletStart.Mode.ColdWallet AppRoute.CreateWalletStart.Mode.ColdWallet

View file

@ -1,26 +0,0 @@
package com.tangem.feature.referral.data
import androidx.datastore.preferences.core.booleanPreferencesKey
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.feature.referral.domain.MobileWalletPromoRepository
import javax.inject.Inject
internal class DefaultMobileWalletPromoRepository @Inject constructor(
private val appPreferencesStore: AppPreferencesStore,
) : MobileWalletPromoRepository {
override suspend fun shouldShowMobileWalletPromo(): Boolean {
return appPreferencesStore.getSyncOrDefault(key = SHOULD_SHOW_MOBILE_WALLET_PROMO_KEY, default = false)
}
override suspend fun setShouldShowMobileWalletPromo(shouldShowPromo: Boolean) {
appPreferencesStore.editData { preferences ->
preferences[SHOULD_SHOW_MOBILE_WALLET_PROMO_KEY] = shouldShowPromo
}
}
private companion object {
val SHOULD_SHOW_MOBILE_WALLET_PROMO_KEY = booleanPreferencesKey("should_show_mobile_wallet_promo")
}
}

View file

@ -2,13 +2,10 @@ package com.tangem.feature.referral.di
import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.feature.referral.converters.ReferralConverter import com.tangem.feature.referral.converters.ReferralConverter
import com.tangem.feature.referral.data.DefaultMobileWalletPromoRepository
import com.tangem.feature.referral.data.ExternalReferralRepository import com.tangem.feature.referral.data.ExternalReferralRepository
import com.tangem.feature.referral.data.ReferralRepositoryImpl import com.tangem.feature.referral.data.ReferralRepositoryImpl
import com.tangem.feature.referral.domain.MobileWalletPromoRepository
import com.tangem.feature.referral.domain.ReferralRepository import com.tangem.feature.referral.domain.ReferralRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module import dagger.Module
@ -56,11 +53,4 @@ class ReferralRepositoryModule {
excludedBlockchains = excludedBlockchains, excludedBlockchains = excludedBlockchains,
) )
} }
@Provides
@Singleton
fun provideMobileWalletPromoRepository(appPreferencesStore: AppPreferencesStore): MobileWalletPromoRepository =
DefaultMobileWalletPromoRepository(
appPreferencesStore = appPreferencesStore,
)
} }

View file

@ -1,8 +0,0 @@
package com.tangem.feature.referral.domain
interface MobileWalletPromoRepository {
suspend fun shouldShowMobileWalletPromo(): Boolean
suspend fun setShouldShowMobileWalletPromo(shouldShowPromo: Boolean)
}

View file

@ -1,18 +0,0 @@
package com.tangem.feature.referral.domain
import arrow.core.Either
import com.tangem.domain.common.wallets.UserWalletsListRepository
import javax.inject.Inject
class SetShouldShowMobileWalletPromoUseCase @Inject constructor(
private val mobileWalletPromoRepository: MobileWalletPromoRepository,
private val userWalletsListRepository: UserWalletsListRepository,
) {
suspend operator fun invoke(shouldShowPromo: Boolean): Either<Throwable, Unit> = Either.catch {
val wallets = userWalletsListRepository.userWallets.value
if (wallets.isNullOrEmpty()) {
mobileWalletPromoRepository.setShouldShowMobileWalletPromo(shouldShowPromo)
}
}
}

View file

@ -1,12 +0,0 @@
package com.tangem.feature.referral.domain
import javax.inject.Inject
class ShouldShowMobileWalletPromoUseCase @Inject constructor(
private val mobileWalletPromoRepository: MobileWalletPromoRepository,
) {
suspend operator fun invoke(): Boolean {
return mobileWalletPromoRepository.shouldShowMobileWalletPromo()
}
}