Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-22 16:11:10 +03:00
parent 9fd13fdc46
commit 2f48642a14
9 changed files with 376 additions and 137 deletions

View file

@ -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"
}
}

View file

@ -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<String?, Any?>) {
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"
}
}

View file

@ -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<String?, Any?>?) {
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<String?, String?>?) {
Timber.i("AppsFlyer app open attribution: ${p0.orEmpty()}")
}
override fun onAttributionFailure(p0: String?) {
Timber.e("AppsFlyer attribution failure: ${p0.orEmpty()}")
}
}

View file

@ -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<String?, Any?>?) {
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<String?, String?>?) {
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")