Updated on 2026-08-14
This commit is contained in:
parent
58428d773b
commit
d77875be51
15 changed files with 414 additions and 53 deletions
|
|
@ -46,6 +46,7 @@ import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
|||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
|
||||
import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerClient
|
||||
import com.tangem.tap.common.log.TangemAppLoggerInitializer
|
||||
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
|
|
@ -157,4 +158,6 @@ interface ApplicationEntryPoint {
|
|||
fun getTrackingContextProxy(): TrackingContextProxy
|
||||
|
||||
fun getABTestsManager(): ABTestsManager
|
||||
|
||||
fun getAppsFlyerClientFactory(): AppsFlyerClient.Factory
|
||||
}
|
||||
|
|
@ -66,6 +66,7 @@ import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
|
|||
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
|
||||
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
|
||||
import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerAnalyticsHandler
|
||||
import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerClient
|
||||
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
|
||||
import com.tangem.tap.common.images.createCoilImageLoader
|
||||
import com.tangem.tap.common.log.TangemAppLoggerInitializer
|
||||
|
|
@ -248,6 +249,9 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
|||
private val abTestsManager: ABTestsManager
|
||||
get() = entryPoint.getABTestsManager()
|
||||
|
||||
private val appsFlyerClientFactory: AppsFlyerClient.Factory
|
||||
get() = entryPoint.getAppsFlyerClientFactory()
|
||||
|
||||
// endregion
|
||||
|
||||
private val appScope = MainScope()
|
||||
|
|
@ -421,7 +425,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
|||
val factory = AnalyticsFactory()
|
||||
factory.addHandlerBuilder(AmplitudeAnalyticsHandler.Builder())
|
||||
factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder())
|
||||
factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder())
|
||||
factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder(appsFlyerClientFactory))
|
||||
|
||||
factory.addFilter(oneTimeEventFilter)
|
||||
factory.addFilter(AppsFlyerEventFilter())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
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,
|
||||
) : 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)
|
||||
}
|
||||
DeepLinkResult.Status.NOT_FOUND -> {
|
||||
Timber.i("No deep link found")
|
||||
}
|
||||
DeepLinkResult.Status.ERROR -> {
|
||||
Timber.e("Deep link error: ${p0.error}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.store(
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
|
@ -6,59 +6,36 @@ 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.handlers.firebase.UnderscoreAnalyticsEventConverter
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import timber.log.Timber
|
||||
|
||||
interface AppsFlyerAnalyticsClient : EventLogger, UserIdHolder
|
||||
|
||||
internal class AppsFlyerClient(
|
||||
private val context: Context,
|
||||
key: String,
|
||||
class AppsFlyerClient @AssistedInject constructor(
|
||||
@Assisted apiKey: String,
|
||||
@ApplicationContext private val context: Context,
|
||||
appsFlyerDeepLinkListener: AppsFlyerDeepLinkListener,
|
||||
) : AppsFlyerAnalyticsClient {
|
||||
|
||||
private val appsFlyerLib: AppsFlyerLib = AppsFlyerLib.getInstance()
|
||||
private val eventConverter = UnderscoreAnalyticsEventConverter()
|
||||
private val logEventListener = object : AppsFlyerRequestListener {
|
||||
override fun onSuccess() {
|
||||
Timber.tag("AppsFlyerClient").i("AppsFlyerRequestListener send")
|
||||
}
|
||||
|
||||
override fun onError(p0: Int, p1: String) {
|
||||
Timber.tag("AppsFlyerClient").e("AppsFlyerRequestListener onError: $p0, $p1")
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val conversionListener = object : 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()}")
|
||||
}
|
||||
}
|
||||
|
||||
appsFlyerLib.run {
|
||||
with(appsFlyerLib) {
|
||||
setAppId(context.packageName)
|
||||
setDebugLog(true)
|
||||
|
||||
init(key, conversionListener, context)
|
||||
Timber.i("Starting AppsFlyer SDK")
|
||||
start(context, key, object : AppsFlyerRequestListener {
|
||||
override fun onSuccess() {
|
||||
Timber.i("AppsFlyer initialized successfully")
|
||||
}
|
||||
init(apiKey, ConversionListener, context)
|
||||
|
||||
override fun onError(p0: Int, p1: String) {
|
||||
Timber.e("AppsFlyer initialization error: $p0, $p1")
|
||||
}
|
||||
})
|
||||
subscribeForDeepLink(appsFlyerDeepLinkListener)
|
||||
|
||||
Timber.i("Starting AppsFlyer SDK")
|
||||
start(context, apiKey, InitializationListener)
|
||||
Timber.i("AppsFlyer SDK started")
|
||||
}
|
||||
}
|
||||
|
|
@ -77,7 +54,47 @@ internal class AppsFlyerClient(
|
|||
context,
|
||||
event,
|
||||
eventConverter.convertEventParams(params),
|
||||
logEventListener,
|
||||
LogEventListener,
|
||||
)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
override fun onError(p0: Int, p1: String) {
|
||||
Timber.e("AppsFlyer initialization error: $p0, $p1")
|
||||
}
|
||||
}
|
||||
|
||||
private object LogEventListener : AppsFlyerRequestListener {
|
||||
override fun onSuccess() {
|
||||
Timber.tag("AppsFlyerClient").i("AppsFlyerRequestListener send")
|
||||
}
|
||||
|
||||
override fun onError(p0: Int, p1: String) {
|
||||
Timber.tag("AppsFlyerClient").e("AppsFlyerRequestListener onError: $p0, $p1")
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(apiKey: String): AppsFlyerClient
|
||||
}
|
||||
}
|
||||
|
|
@ -47,7 +47,10 @@ class AppsFlyerAnalyticsHandler(
|
|||
const val ID = "AppsFlyer"
|
||||
}
|
||||
|
||||
class Builder : AnalyticsHandlerBuilder {
|
||||
internal class Builder(
|
||||
private val appsFlyerClientFactory: AppsFlyerClient.Factory,
|
||||
) : AnalyticsHandlerBuilder {
|
||||
|
||||
init {
|
||||
Timber.tag("AppsFlyer").i("AppsFlyer Analytics Handler Builder created")
|
||||
}
|
||||
|
|
@ -58,7 +61,7 @@ class AppsFlyerAnalyticsHandler(
|
|||
AppsFlyerLogClient(data.jsonConverter)
|
||||
} else {
|
||||
Timber.tag("AppsFlyer").i("AppsFlyer log disabled, real client created")
|
||||
AppsFlyerClient(data.application, data.config.appsFlyerApiKey)
|
||||
appsFlyerClientFactory.create(apiKey = data.config.appsFlyerApiKey)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue