From 2f48642a142d773c08e8b83b5b5d4513051669c4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 22 Jan 2026 16:11:10 +0300 Subject: [PATCH] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../appsflyer/AppsFlyerDeepLinkListener.kt | 71 +------- .../AppsFlyerReferralParamsHandler.kt | 86 +++++++++ .../appsflyer/TangemAFConversionListener.kt | 32 ++++ .../appsflyer/AppsFlyerAnalyticsClient.kt | 20 +-- .../AppsFlyerDeepLinkListenerTest.kt | 71 +++----- .../AppsFlyerReferralParamsHandlerTest.kt | 164 ++++++++++++++++++ .../TangemAFConversionListenerTest.kt | 63 +++++++ gradle/dependencies.toml | 4 + 9 files changed, 376 insertions(+), 137 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/appsflyer/TangemAFConversionListener.kt create mode 100644 app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandlerTest.kt create mode 100644 app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/TangemAFConversionListenerTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 61936049d7..e6002f64e0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -366,6 +366,8 @@ dependencies { implementation(deps.googlePlay.review) implementation(deps.googlePlay.review.ktx) implementation(deps.googlePlay.services.wallet) + implementation(deps.googlePlay.services) + implementation(deps.googlePlay.advertising) coreLibraryDesugaring(deps.desugar) implementation(deps.timber) implementation(deps.reKotlin) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListener.kt b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListener.kt index f656143400..ae2547a4a9 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListener.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListener.kt @@ -1,36 +1,20 @@ package com.tangem.tap.common.analytics.appsflyer -import androidx.annotation.VisibleForTesting -import com.appsflyer.deeplink.DeepLink import com.appsflyer.deeplink.DeepLinkListener import com.appsflyer.deeplink.DeepLinkResult -import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore -import com.tangem.domain.wallets.models.AppsFlyerConversionData -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.contract @Singleton class AppsFlyerDeepLinkListener @Inject constructor( - private val appsFlyerConversionStore: AppsFlyerConversionStore, - dispatchers: CoroutineDispatcherProvider, + private val referralParamsHandler: AppsFlyerReferralParamsHandler, ) : DeepLinkListener { - private val coroutineScope = CoroutineScope(dispatchers.io + SupervisorJob()) - private val mutex = Mutex() - override fun onDeepLinking(p0: DeepLinkResult) { when (p0.status) { DeepLinkResult.Status.FOUND -> { - onDeepLinkFound(p0.deepLink) + referralParamsHandler.handle(deepLink = p0.deepLink) } DeepLinkResult.Status.NOT_FOUND -> { Timber.i("No deep link found") @@ -40,55 +24,4 @@ class AppsFlyerDeepLinkListener @Inject constructor( } } } - - private fun onDeepLinkFound(deepLink: DeepLink) { - if (deepLink.deepLinkValue != REFERRAL_DEEP_LINK_VALUE) { - Timber.i("Ignoring deep link with value: ${deepLink.deepLinkValue ?: "null"}") - return - } - - val refcode = deepLink.getStringValue(REFCODE_PARAM_KEY) - val campaign = deepLink.getStringValue(CAMPAIGN_PARAM_KEY) - - @Suppress("NullableToStringCall") - Timber.i("refcode=$refcode\ncampaign=$campaign") - - if (!isValidParam(refcode)) { - Timber.e("Deep link conversion data is invalid") - return - } - - storeConversionData(refcode = refcode, campaign = campaign) - } - - @OptIn(ExperimentalContracts::class) - private fun isValidParam(value: String?): Boolean { - contract { - returns(true) implies (value != null) - } - - return value != null && value.isNotBlank() && !value.equals("null", ignoreCase = true) - } - - private fun storeConversionData(refcode: String, campaign: String?) { - coroutineScope.launch { - mutex.withLock { - appsFlyerConversionStore.storeIfAbsent( - value = AppsFlyerConversionData(refcode = refcode, campaign = campaign), - ) - } - } - } - - companion object Companion { - - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - const val REFERRAL_DEEP_LINK_VALUE = "referral" - - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - const val REFCODE_PARAM_KEY = "deep_link_sub1" - - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - const val CAMPAIGN_PARAM_KEY = "deep_link_sub2" - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt new file mode 100644 index 0000000000..21c04dbcac --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt @@ -0,0 +1,86 @@ +package com.tangem.tap.common.analytics.appsflyer + +import com.appsflyer.deeplink.DeepLink +import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore +import com.tangem.domain.wallets.models.AppsFlyerConversionData +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +@Singleton +class AppsFlyerReferralParamsHandler @Inject constructor( + private val appsFlyerConversionStore: AppsFlyerConversionStore, + dispatchers: CoroutineDispatcherProvider, +) { + + private val coroutineScope = CoroutineScope(dispatchers.io + SupervisorJob()) + private val mutex = Mutex() + + fun handle(deepLink: DeepLink) { + handle( + deepLinkValue = deepLink.deepLinkValue, + deepLinkSub1 = deepLink.getStringValue(DEEP_LINK_SUB_1), + deepLinkSub2 = deepLink.getStringValue(DEEP_LINK_SUB_2), + ) + } + + fun handle(params: Map) { + handle( + deepLinkValue = params[DEEP_LINK_VALUE] as? String, + deepLinkSub1 = params[DEEP_LINK_SUB_1] as? String, + deepLinkSub2 = params[DEEP_LINK_SUB_2] as? String, + ) + } + + private fun handle(deepLinkValue: String?, deepLinkSub1: String?, deepLinkSub2: String?) { + if (deepLinkValue != REFERRAL_DEEP_LINK_VALUE) { + Timber.i("Ignoring deep link with value: ${deepLinkValue ?: "null"}") + return + } + + @Suppress("NullableToStringCall") + Timber.i("refcode=$deepLinkSub1\ncampaign=$deepLinkSub2") + + if (!isValidParam(deepLinkSub1)) { + Timber.e("Deeplink conversion data is invalid") + return + } + + storeConversionData(refcode = deepLinkSub1, campaign = deepLinkSub2) + } + + @OptIn(ExperimentalContracts::class) + private fun isValidParam(value: String?): Boolean { + contract { + returns(true) implies (value != null) + } + + return value != null && value.isNotBlank() && !value.equals("null", ignoreCase = true) + } + + private fun storeConversionData(refcode: String, campaign: String?) { + coroutineScope.launch { + mutex.withLock { + appsFlyerConversionStore.storeIfAbsent( + value = AppsFlyerConversionData(refcode = refcode, campaign = campaign), + ) + } + } + } + + private companion object { + + const val REFERRAL_DEEP_LINK_VALUE = "referral" + const val DEEP_LINK_VALUE = "deep_link_value" + const val DEEP_LINK_SUB_1 = "deep_link_sub1" + const val DEEP_LINK_SUB_2 = "deep_link_sub2" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/TangemAFConversionListener.kt b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/TangemAFConversionListener.kt new file mode 100644 index 0000000000..a051eefeb9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/TangemAFConversionListener.kt @@ -0,0 +1,32 @@ +package com.tangem.tap.common.analytics.appsflyer + +import com.appsflyer.AppsFlyerConversionListener +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class TangemAFConversionListener @Inject constructor( + private val referralParamsHandler: AppsFlyerReferralParamsHandler, +) : AppsFlyerConversionListener { + + override fun onConversionDataSuccess(p0: Map?) { + Timber.i("AppsFlyer conversion data success: ${p0.orEmpty()}") + + if (p0 == null) return + + referralParamsHandler.handle(params = p0) + } + + override fun onConversionDataFail(p0: String?) { + Timber.e("AppsFlyer conversion data failure: ${p0.orEmpty()}") + } + + override fun onAppOpenAttribution(p0: Map?) { + Timber.i("AppsFlyer app open attribution: ${p0.orEmpty()}") + } + + override fun onAttributionFailure(p0: String?) { + Timber.e("AppsFlyer attribution failure: ${p0.orEmpty()}") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsClient.kt index 07b0b64fc1..77227aa7c2 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsClient.kt @@ -1,12 +1,12 @@ package com.tangem.tap.common.analytics.handlers.appsflyer import android.content.Context -import com.appsflyer.AppsFlyerConversionListener import com.appsflyer.AppsFlyerLib import com.appsflyer.attribution.AppsFlyerRequestListener import com.tangem.core.analytics.api.EventLogger import com.tangem.core.analytics.api.UserIdHolder import com.tangem.tap.common.analytics.appsflyer.AppsFlyerDeepLinkListener +import com.tangem.tap.common.analytics.appsflyer.TangemAFConversionListener import com.tangem.tap.common.analytics.handlers.firebase.UnderscoreAnalyticsEventConverter import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -20,6 +20,7 @@ class AppsFlyerClient @AssistedInject constructor( @Assisted apiKey: String, @ApplicationContext private val context: Context, appsFlyerDeepLinkListener: AppsFlyerDeepLinkListener, + tangemAFConversionListener: TangemAFConversionListener, ) : AppsFlyerAnalyticsClient { private val appsFlyerLib: AppsFlyerLib = AppsFlyerLib.getInstance() @@ -32,7 +33,7 @@ class AppsFlyerClient @AssistedInject constructor( subscribeForDeepLink(appsFlyerDeepLinkListener) - init(apiKey, ConversionListener, context) + init(apiKey, tangemAFConversionListener, context) Timber.i("Starting AppsFlyer SDK") start(context, apiKey, InitializationListener) @@ -58,21 +59,6 @@ class AppsFlyerClient @AssistedInject constructor( ) } - private object ConversionListener : AppsFlyerConversionListener { - override fun onConversionDataSuccess(p0: Map?) { - Timber.i("AppsFlyer conversion data success: ${p0.orEmpty()}") - } - override fun onConversionDataFail(p0: String?) { - Timber.e("AppsFlyer conversion data failure: ${p0.orEmpty()}") - } - override fun onAppOpenAttribution(p0: Map?) { - Timber.i("AppsFlyer app open attribution: ${p0.orEmpty()}") - } - override fun onAttributionFailure(p0: String?) { - Timber.e("AppsFlyer attribution failure: ${p0.orEmpty()}") - } - } - private object InitializationListener : AppsFlyerRequestListener { override fun onSuccess() { Timber.d("AppsFlyer initialized successfully") diff --git a/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListenerTest.kt b/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListenerTest.kt index cce586770c..20f770e186 100644 --- a/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListenerTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListenerTest.kt @@ -2,16 +2,8 @@ package com.tangem.tap.common.analytics.appsflyer import com.appsflyer.deeplink.DeepLink import com.appsflyer.deeplink.DeepLinkResult -import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore -import com.tangem.domain.wallets.models.AppsFlyerConversionData -import com.tangem.tap.common.analytics.appsflyer.AppsFlyerDeepLinkListener.Companion.CAMPAIGN_PARAM_KEY -import com.tangem.tap.common.analytics.appsflyer.AppsFlyerDeepLinkListener.Companion.REFCODE_PARAM_KEY import com.tangem.test.core.ProvideTestModels -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.clearMocks -import io.mockk.coVerify -import io.mockk.every -import io.mockk.mockk +import io.mockk.* import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.TestInstance @@ -23,27 +15,29 @@ import org.junit.jupiter.params.ParameterizedTest @TestInstance(TestInstance.Lifecycle.PER_CLASS) class AppsFlyerDeepLinkListenerTest { - private val appsFlyerConversionStore: AppsFlyerConversionStore = mockk(relaxUnitFun = true) + private val referralParamsHandler: AppsFlyerReferralParamsHandler = mockk() private val listener = AppsFlyerDeepLinkListener( - appsFlyerConversionStore = appsFlyerConversionStore, - dispatchers = TestingCoroutineDispatcherProvider(), + referralParamsHandler = referralParamsHandler, ) @AfterEach fun tearDown() { - clearMocks(appsFlyerConversionStore) + clearMocks(referralParamsHandler) } @ParameterizedTest @ProvideTestModels fun onDeepLinking(model: OnDeepLinkingModel) = runTest { + if (model.shouldHandle) { + every { referralParamsHandler.handle(deepLink = model.deepLinkResult.deepLink) } just Runs + } + listener.onDeepLinking(p0 = model.deepLinkResult) - if (model.shouldStore) { - val value = AppsFlyerConversionData(refcode = SUCCESS_REFCODE, campaign = SUCCESS_CAMPAIGN) - coVerify { appsFlyerConversionStore.storeIfAbsent(value = value) } + if (model.shouldHandle) { + coVerify { referralParamsHandler.handle(deepLink = model.deepLinkResult.deepLink) } } else { - coVerify(inverse = true) { appsFlyerConversionStore.storeIfAbsent(value = any()) } + coVerify(inverse = true) { referralParamsHandler.handle(deepLink = any()) } } } @@ -52,48 +46,28 @@ class AppsFlyerDeepLinkListenerTest { // DeepLinkResult.Status.NOT_FOUND OnDeepLinkingModel( deepLinkResult = DeepLinkResult(null, null), - shouldStore = false, + shouldHandle = false, ), // DeepLinkResult.Status.ERROR OnDeepLinkingModel( deepLinkResult = DeepLinkResult(null, DeepLinkResult.Error.NETWORK), - shouldStore = false, + shouldHandle = false, ), // DeepLinkResult.Status.FOUND OnDeepLinkingModel( deepLinkResult = createFoundDeepLink( - deepLinkValue = AppsFlyerDeepLinkListener.REFERRAL_DEEP_LINK_VALUE, - refcode = SUCCESS_REFCODE, - campaign = SUCCESS_CAMPAIGN, + deepLinkValue = "deep_link_value", + refcode = "refcode", + campaign = "campaign", ), - shouldStore = true, - ), - OnDeepLinkingModel( - deepLinkResult = createFoundDeepLink( - deepLinkValue = AppsFlyerDeepLinkListener.REFERRAL_DEEP_LINK_VALUE, - refcode = "", - campaign = SUCCESS_CAMPAIGN, - ), - shouldStore = false, - ), - OnDeepLinkingModel( - deepLinkResult = createFoundDeepLink( - deepLinkValue = AppsFlyerDeepLinkListener.REFERRAL_DEEP_LINK_VALUE, - refcode = null, - campaign = SUCCESS_CAMPAIGN, - ), - shouldStore = false, - ), - OnDeepLinkingModel( - deepLinkResult = createFoundDeepLink(deepLinkValue = "some_other_deep_link_value"), - shouldStore = false, + shouldHandle = true, ), ) } data class OnDeepLinkingModel( val deepLinkResult: DeepLinkResult, - val shouldStore: Boolean, + val shouldHandle: Boolean, ) private fun createFoundDeepLink( @@ -103,8 +77,8 @@ class AppsFlyerDeepLinkListenerTest { ): DeepLinkResult { val deeplink = mockk { every { this@mockk.deepLinkValue } returns deepLinkValue - every { this@mockk.getStringValue(REFCODE_PARAM_KEY) } returns refcode - every { this@mockk.getStringValue(CAMPAIGN_PARAM_KEY) } returns campaign + every { this@mockk.getStringValue("deep_link_sub1") } returns refcode + every { this@mockk.getStringValue("deep_link_sub2") } returns campaign } return mockk { @@ -112,9 +86,4 @@ class AppsFlyerDeepLinkListenerTest { every { this@mockk.deepLink } returns deeplink } } - - private companion object { - const val SUCCESS_REFCODE = "valid_refcode" - const val SUCCESS_CAMPAIGN = "valid_campaign" - } } \ No newline at end of file diff --git a/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandlerTest.kt b/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandlerTest.kt new file mode 100644 index 0000000000..41e17cbddc --- /dev/null +++ b/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandlerTest.kt @@ -0,0 +1,164 @@ +package com.tangem.tap.common.analytics.appsflyer + +import com.appsflyer.deeplink.DeepLink +import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore +import com.tangem.domain.wallets.models.AppsFlyerConversionData +import com.tangem.test.core.ProvideTestModels +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.clearMocks +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AppsFlyerReferralParamsHandlerTest { + + private val appsFlyerConversionStore: AppsFlyerConversionStore = mockk(relaxUnitFun = true) + private val handler = AppsFlyerReferralParamsHandler( + appsFlyerConversionStore = appsFlyerConversionStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @AfterEach + fun tearDown() { + clearMocks(appsFlyerConversionStore) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class HandleDeepLink { + + @ParameterizedTest + @ProvideTestModels + fun handle(model: HandleDeepLinkModel) = runTest { + handler.handle(deepLink = model.deepLink) + + if (model.shouldStore) { + val value = AppsFlyerConversionData(refcode = SUCCESS_REFCODE, campaign = SUCCESS_CAMPAIGN) + coVerify { appsFlyerConversionStore.storeIfAbsent(value = value) } + } else { + coVerify(inverse = true) { appsFlyerConversionStore.storeIfAbsent(value = any()) } + } + } + + private fun provideTestModels(): List { + return listOf( + HandleDeepLinkModel( + deepLink = createDeepLink( + deepLinkValue = "referral", + refcode = SUCCESS_REFCODE, + campaign = SUCCESS_CAMPAIGN, + ), + shouldStore = true, + ), + HandleDeepLinkModel( + deepLink = createDeepLink(deepLinkValue = "some_other_deep_link_value"), + shouldStore = false, + ), + HandleDeepLinkModel( + deepLink = createDeepLink(deepLinkValue = ""), + shouldStore = false, + ), + HandleDeepLinkModel( + deepLink = createDeepLink( + deepLinkValue = "referral", + refcode = "", + campaign = SUCCESS_CAMPAIGN, + ), + shouldStore = false, + ), + HandleDeepLinkModel( + deepLink = createDeepLink( + deepLinkValue = "referral", + refcode = null, + campaign = SUCCESS_CAMPAIGN, + ), + shouldStore = false, + ), + ) + } + + private fun createDeepLink( + deepLinkValue: String, + refcode: String? = null, + campaign: String? = null, + ): DeepLink { + return mockk { + every { this@mockk.deepLinkValue } returns deepLinkValue + every { this@mockk.getStringValue("deep_link_sub1") } returns refcode + every { this@mockk.getStringValue("deep_link_sub2") } returns campaign + } + } + } + + data class HandleDeepLinkModel(val deepLink: DeepLink, val shouldStore: Boolean) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class HandleParams { + + @ParameterizedTest + @ProvideTestModels + fun handle(model: HandleParamsModel) = runTest { + handler.handle(params = model.params) + + if (model.shouldStore) { + val value = AppsFlyerConversionData(refcode = SUCCESS_REFCODE, campaign = SUCCESS_CAMPAIGN) + coVerify { appsFlyerConversionStore.storeIfAbsent(value = value) } + } else { + coVerify(inverse = true) { appsFlyerConversionStore.storeIfAbsent(value = any()) } + } + } + + private fun provideTestModels(): List { + return listOf( + HandleParamsModel( + params = mapOf( + "deep_link_value" to "referral", + "deep_link_sub1" to SUCCESS_REFCODE, + "deep_link_sub2" to SUCCESS_CAMPAIGN, + ), + shouldStore = true, + ), + HandleParamsModel( + params = mapOf("deep_link_value" to "some_other_deep_link_value"), + shouldStore = false, + ), + HandleParamsModel( + params = mapOf("deep_link_value" to ""), + shouldStore = false, + ), + HandleParamsModel( + params = mapOf( + "deep_link_value" to "referral", + "deep_link_sub1" to "", + "deep_link_sub2" to SUCCESS_CAMPAIGN, + ), + shouldStore = false, + ), + HandleParamsModel( + params = mapOf( + "deep_link_value" to "referral", + "deep_link_sub2" to SUCCESS_CAMPAIGN, + ), + shouldStore = false, + ), + ) + } + } + + data class HandleParamsModel(val params: Map, val shouldStore: Boolean) + + private companion object Companion { + const val SUCCESS_REFCODE = "valid_refcode" + const val SUCCESS_CAMPAIGN = "valid_campaign" + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/TangemAFConversionListenerTest.kt b/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/TangemAFConversionListenerTest.kt new file mode 100644 index 0000000000..f66b08aa16 --- /dev/null +++ b/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/TangemAFConversionListenerTest.kt @@ -0,0 +1,63 @@ +package com.tangem.tap.common.analytics.appsflyer + +import com.tangem.test.core.ProvideTestModels +import io.mockk.clearMocks +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TangemAFConversionListenerTest { + + private val referralParamsHandler: AppsFlyerReferralParamsHandler = mockk(relaxUnitFun = true) + private val listener = TangemAFConversionListener( + referralParamsHandler = referralParamsHandler, + ) + + @AfterEach + fun tearDown() { + clearMocks(referralParamsHandler) + } + + @ParameterizedTest + @ProvideTestModels + fun onConversionDataSuccess(model: OnConversionDataSuccessModel) = runTest { + listener.onConversionDataSuccess(p0 = model.params) + + if (model.shouldHandle) { + coVerify { referralParamsHandler.handle(params = model.params!!) } + } else { + coVerify(inverse = true) { referralParamsHandler.handle(params = any()) } + } + } + + private fun provideTestModels(): List { + return listOf( + OnConversionDataSuccessModel( + params = null, + shouldHandle = false, + ), + OnConversionDataSuccessModel( + params = emptyMap(), + shouldHandle = true, + ), + OnConversionDataSuccessModel( + params = mapOf( + "deep_link_value" to "referral", + ), + shouldHandle = true, + ), + ) + } + + data class OnConversionDataSuccessModel( + val params: Map?, + val shouldHandle: Boolean, + ) +} \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index f284f5dc0c..45d0d78603 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -5,6 +5,8 @@ androidGradlePlugin = "8.10.1" firebaseCrashlytics = "3.0.1" googleServices = "4.4.1" +playServices = "18.10.0" +playServicesAds = "18.2.0" kotlin = "2.1.10" ksp = "2.1.10-1.0.30" firebasePerf = "1.4.2" @@ -254,6 +256,8 @@ desugar = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desuga googlePlay-review = { module = "com.google.android.play:review", version.ref = "googlePlayReview" } googlePlay-review-ktx = { module = "com.google.android.play:review-ktx", version.ref = "googlePlayReviewKtx" } googlePlay-services-wallet = { module = "com.google.android.gms:play-services-wallet", version.ref = "googlePlayServicesWallet" } +googlePlay-services = { module = "com.google.android.gms:play-services-base", version.ref = "playServices" } +googlePlay-advertising = { module = "com.google.android.gms:play-services-ads-identifier", version.ref = "playServicesAds" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-core = { module = "com.google.dagger:hilt-core", version.ref = "hilt" } hilt-kapt = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }