Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-13 10:51:26 +03:00
parent 58428d773b
commit d77875be51
15 changed files with 414 additions and 53 deletions

View file

@ -108,6 +108,9 @@ configurations.androidTestImplementation {
exclude(module = "protobuf-lite") exclude(module = "protobuf-lite")
} }
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies { dependencies {
implementation(projects.domain.legacy) implementation(projects.domain.legacy)
@ -393,11 +396,10 @@ dependencies {
implementation(files("libs/dexprotector-annotations.jar")) implementation(files("libs/dexprotector-annotations.jar"))
/** Testing libraries */ /** Testing libraries */
testImplementation(projects.test.core)
testImplementation(projects.common.test) testImplementation(projects.common.test)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit) testImplementation(deps.test.junit)
testImplementation(deps.test.mockk) testRuntimeOnly(deps.test.junit5.engine)
testImplementation(deps.test.truth)
androidTestImplementation(deps.test.junit.android) androidTestImplementation(deps.test.junit.android)
androidTestImplementation(deps.test.espresso) androidTestImplementation(deps.test.espresso)
androidTestImplementation(deps.test.espresso.intents) androidTestImplementation(deps.test.espresso.intents)

View file

@ -11,9 +11,15 @@
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID" /> <uses-permission android:name="com.google.android.gms.permission.AD_ID" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" tools:node="remove" /> <uses-permission
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" tools:node="remove" /> android:name="android.permission.ACCESS_FINE_LOCATION"
<uses-permission android:name="android.permission.RECORD_AUDIO" tools:node="remove" /> tools:node="remove" />
<uses-permission
android:name="android.permission.ACCESS_COARSE_LOCATION"
tools:node="remove" />
<uses-permission
android:name="android.permission.RECORD_AUDIO"
tools:node="remove" />
<uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" /> <uses-feature android:name="android.hardware.camera.autofocus" />
@ -36,7 +42,6 @@
<application <application
android:name="com.tangem.tap.TangemHiltApplication" android:name="com.tangem.tap.TangemHiltApplication"
android:theme="@style/AppTheme"
android:allowBackup="false" android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="true"
@ -49,6 +54,7 @@
android:resizeableActivity="@bool/resizeable_activity" android:resizeableActivity="@bool/resizeable_activity"
android:roundIcon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning" tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:allowBackup, android:dataExtractionRules, android:fullBackupContent, android:label, android:largeHeap"> tools:replace="android:allowBackup, android:dataExtractionRules, android:fullBackupContent, android:label, android:largeHeap">
@ -286,9 +292,20 @@
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data
android:scheme="https"
android:host="tangem.com" android:host="tangem.com"
android:path="/pay-app" /> android:path="/pay-app"
android:scheme="https" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="tangem.onelink.me"
android:scheme="https" />
</intent-filter> </intent-filter>
</activity> </activity>
@ -326,6 +343,6 @@
</application> </application>
<queries> <queries>
<!-- Needed from Android 11 to open Google Wallet for payment with Visa card --> <!-- Needed from Android 11 to open Google Wallet for payment with Visa card -->
<package android:name="com.google.android.apps.walletnfcrel" /> <package android:name="com.google.android.apps.walletnfcrel" />
</queries> </queries>
</manifest> </manifest>

View file

@ -46,6 +46,7 @@ import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler 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.common.log.TangemAppLoggerInitializer
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.AppStateHolder
@ -157,4 +158,6 @@ interface ApplicationEntryPoint {
fun getTrackingContextProxy(): TrackingContextProxy fun getTrackingContextProxy(): TrackingContextProxy
fun getABTestsManager(): ABTestsManager fun getABTestsManager(): ABTestsManager
fun getAppsFlyerClientFactory(): AppsFlyerClient.Factory
} }

View file

@ -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.BlockchainExceptionHandler
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler 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.AppsFlyerAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerClient
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
import com.tangem.tap.common.images.createCoilImageLoader import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.common.log.TangemAppLoggerInitializer
@ -248,6 +249,9 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
private val abTestsManager: ABTestsManager private val abTestsManager: ABTestsManager
get() = entryPoint.getABTestsManager() get() = entryPoint.getABTestsManager()
private val appsFlyerClientFactory: AppsFlyerClient.Factory
get() = entryPoint.getAppsFlyerClientFactory()
// endregion // endregion
private val appScope = MainScope() private val appScope = MainScope()
@ -421,7 +425,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
val factory = AnalyticsFactory() val factory = AnalyticsFactory()
factory.addHandlerBuilder(AmplitudeAnalyticsHandler.Builder()) factory.addHandlerBuilder(AmplitudeAnalyticsHandler.Builder())
factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder()) factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder())
factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder()) factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder(appsFlyerClientFactory))
factory.addFilter(oneTimeEventFilter) factory.addFilter(oneTimeEventFilter)
factory.addFilter(AppsFlyerEventFilter()) factory.addFilter(AppsFlyerEventFilter())

View file

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

View file

@ -6,59 +6,36 @@ import com.appsflyer.AppsFlyerLib
import com.appsflyer.attribution.AppsFlyerRequestListener import com.appsflyer.attribution.AppsFlyerRequestListener
import com.tangem.core.analytics.api.EventLogger import com.tangem.core.analytics.api.EventLogger
import com.tangem.core.analytics.api.UserIdHolder 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 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 import timber.log.Timber
interface AppsFlyerAnalyticsClient : EventLogger, UserIdHolder interface AppsFlyerAnalyticsClient : EventLogger, UserIdHolder
internal class AppsFlyerClient( class AppsFlyerClient @AssistedInject constructor(
private val context: Context, @Assisted apiKey: String,
key: String, @ApplicationContext private val context: Context,
appsFlyerDeepLinkListener: AppsFlyerDeepLinkListener,
) : AppsFlyerAnalyticsClient { ) : AppsFlyerAnalyticsClient {
private val appsFlyerLib: AppsFlyerLib = AppsFlyerLib.getInstance() private val appsFlyerLib: AppsFlyerLib = AppsFlyerLib.getInstance()
private val eventConverter = UnderscoreAnalyticsEventConverter() 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 { init {
val conversionListener = object : AppsFlyerConversionListener { with(appsFlyerLib) {
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 {
setAppId(context.packageName) setAppId(context.packageName)
setDebugLog(true) setDebugLog(true)
init(key, conversionListener, context) init(apiKey, ConversionListener, context)
Timber.i("Starting AppsFlyer SDK")
start(context, key, object : AppsFlyerRequestListener {
override fun onSuccess() {
Timber.i("AppsFlyer initialized successfully")
}
override fun onError(p0: Int, p1: String) { subscribeForDeepLink(appsFlyerDeepLinkListener)
Timber.e("AppsFlyer initialization error: $p0, $p1")
} Timber.i("Starting AppsFlyer SDK")
}) start(context, apiKey, InitializationListener)
Timber.i("AppsFlyer SDK started") Timber.i("AppsFlyer SDK started")
} }
} }
@ -77,7 +54,47 @@ internal class AppsFlyerClient(
context, context,
event, event,
eventConverter.convertEventParams(params), 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
}
} }

View file

@ -47,7 +47,10 @@ class AppsFlyerAnalyticsHandler(
const val ID = "AppsFlyer" const val ID = "AppsFlyer"
} }
class Builder : AnalyticsHandlerBuilder { internal class Builder(
private val appsFlyerClientFactory: AppsFlyerClient.Factory,
) : AnalyticsHandlerBuilder {
init { init {
Timber.tag("AppsFlyer").i("AppsFlyer Analytics Handler Builder created") Timber.tag("AppsFlyer").i("AppsFlyer Analytics Handler Builder created")
} }
@ -58,7 +61,7 @@ class AppsFlyerAnalyticsHandler(
AppsFlyerLogClient(data.jsonConverter) AppsFlyerLogClient(data.jsonConverter)
} else { } else {
Timber.tag("AppsFlyer").i("AppsFlyer log disabled, real client created") Timber.tag("AppsFlyer").i("AppsFlyer log disabled, real client created")
AppsFlyerClient(data.application, data.config.appsFlyerApiKey) appsFlyerClientFactory.create(apiKey = data.config.appsFlyerApiKey)
}, },
) )
} }

View file

@ -0,0 +1,120 @@
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 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 AppsFlyerDeepLinkListenerTest {
private val appsFlyerConversionStore: AppsFlyerConversionStore = mockk(relaxUnitFun = true)
private val listener = AppsFlyerDeepLinkListener(
appsFlyerConversionStore = appsFlyerConversionStore,
dispatchers = TestingCoroutineDispatcherProvider(),
)
@AfterEach
fun tearDown() {
clearMocks(appsFlyerConversionStore)
}
@ParameterizedTest
@ProvideTestModels
fun onDeepLinking(model: OnDeepLinkingModel) = runTest {
listener.onDeepLinking(p0 = model.deepLinkResult)
if (model.shouldStore) {
val value = AppsFlyerConversionData(refcode = SUCCESS_REFCODE, campaign = SUCCESS_CAMPAIGN)
coVerify { appsFlyerConversionStore.store(value = value) }
} else {
coVerify(inverse = true) { appsFlyerConversionStore.store(value = any()) }
}
}
private fun provideTestModels(): List<OnDeepLinkingModel> {
return listOf(
// DeepLinkResult.Status.NOT_FOUND
OnDeepLinkingModel(
deepLinkResult = DeepLinkResult(null, null),
shouldStore = false,
),
// DeepLinkResult.Status.ERROR
OnDeepLinkingModel(
deepLinkResult = DeepLinkResult(null, DeepLinkResult.Error.NETWORK),
shouldStore = false,
),
// DeepLinkResult.Status.FOUND
OnDeepLinkingModel(
deepLinkResult = createFoundDeepLink(
deepLinkValue = AppsFlyerDeepLinkListener.REFERRAL_DEEP_LINK_VALUE,
refcode = SUCCESS_REFCODE,
campaign = SUCCESS_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,
),
)
}
data class OnDeepLinkingModel(
val deepLinkResult: DeepLinkResult,
val shouldStore: Boolean,
)
private fun createFoundDeepLink(
deepLinkValue: String,
refcode: String? = null,
campaign: String? = null,
): DeepLinkResult {
val deeplink = mockk<DeepLink> {
every { this@mockk.deepLinkValue } returns deepLinkValue
every { this@mockk.getStringValue(REFCODE_PARAM_KEY) } returns refcode
every { this@mockk.getStringValue(CAMPAIGN_PARAM_KEY) } returns campaign
}
return mockk<DeepLinkResult> {
every { this@mockk.status } returns DeepLinkResult.Status.FOUND
every { this@mockk.deepLink } returns deeplink
}
}
private companion object {
const val SUCCESS_REFCODE = "valid_refcode"
const val SUCCESS_CAMPAIGN = "valid_campaign"
}
}

View file

@ -12,6 +12,8 @@ object WalletIdBodyConverter {
walletId = userWallet.walletId.stringValue, walletId = userWallet.walletId.stringValue,
name = userWallet.name, name = userWallet.name,
walletType = WalletType.from(userWallet), walletType = WalletType.from(userWallet),
refcode = conversionData?.refcode,
campaign = conversionData?.campaign,
cards = publicKeys?.map { publicKeyById -> cards = publicKeys?.map { publicKeyById ->
CardInfoBody( CardInfoBody(
cardId = publicKeyById.key, cardId = publicKeyById.key,

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore
import com.tangem.datasource.local.appsflyer.DefaultAppsFlyerConversionStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object AppsFlyerConversionStoreModule {
@Provides
@Singleton
fun provideAppFlyerConversionStore(appPreferencesStore: AppPreferencesStore): AppsFlyerConversionStore {
return DefaultAppsFlyerConversionStore(appPreferencesStore = appPreferencesStore)
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.local.appsflyer
import com.tangem.domain.wallets.models.AppsFlyerConversionData
interface AppsFlyerConversionStore {
suspend fun get(): AppsFlyerConversionData?
suspend fun store(value: AppsFlyerConversionData)
}

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.local.appsflyer
import com.tangem.domain.wallets.models.AppsFlyerConversionData
import com.tangem.utils.converter.TwoWayConverter
internal object ConversionDataConverter : TwoWayConverter<AppsFlyerConversionData, ConversionDataDTO> {
override fun convert(value: AppsFlyerConversionData): ConversionDataDTO {
return ConversionDataDTO(
refcode = value.refcode,
campaign = value.campaign,
)
}
override fun convertBack(value: ConversionDataDTO): AppsFlyerConversionData {
return AppsFlyerConversionData(
refcode = value.refcode,
campaign = value.campaign,
)
}
}

View file

@ -0,0 +1,42 @@
package com.tangem.datasource.local.appsflyer
import androidx.datastore.preferences.core.stringPreferencesKey
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.domain.wallets.models.AppsFlyerConversionData
import timber.log.Timber
internal class DefaultAppsFlyerConversionStore(
private val appPreferencesStore: AppPreferencesStore,
) : AppsFlyerConversionStore {
override suspend fun get(): AppsFlyerConversionData? {
val dto = appPreferencesStore.getObjectSyncOrNull<ConversionDataDTO>(KEY) ?: return null
return ConversionDataConverter.convertBack(value = dto).also {
Timber.i("Getting conversion data from store: $it")
}
}
override suspend fun store(value: AppsFlyerConversionData) {
Timber.i("Storing conversion data to store: $value")
val dto = ConversionDataConverter.convert(value)
appPreferencesStore.storeObject(KEY, dto)
}
private companion object {
val KEY = stringPreferencesKey("APPS_FLYER_CONVERSION_DATA")
}
}
@JsonClass(generateAdapter = true)
internal data class ConversionDataDTO(
@Json(name = "refcode") val refcode: String,
@Json(name = "campaign") val campaign: String?,
)

View file

@ -72,6 +72,8 @@ class UserTokensSaver(
val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher).copy( val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher).copy(
walletName = userWallet.name, walletName = userWallet.name,
walletType = WalletType.from(userWallet), walletType = WalletType.from(userWallet),
refcode = conversionData?.refcode,
campaign = conversionData?.campaign,
) )
pushLegacy(userWalletId = userWalletId, response = enrichedResponse, onFailSend = onFailSend) pushLegacy(userWalletId = userWalletId, response = enrichedResponse, onFailSend = onFailSend)

View file

@ -0,0 +1,3 @@
package com.tangem.domain.wallets.models
data class AppsFlyerConversionData(val refcode: String, val campaign: String?)