Updated on 2026-08-14
This commit is contained in:
commit
eb9eed8d6c
43 changed files with 1296 additions and 131 deletions
|
|
@ -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)
|
||||||
|
|
@ -363,6 +366,8 @@ dependencies {
|
||||||
implementation(deps.googlePlay.review)
|
implementation(deps.googlePlay.review)
|
||||||
implementation(deps.googlePlay.review.ktx)
|
implementation(deps.googlePlay.review.ktx)
|
||||||
implementation(deps.googlePlay.services.wallet)
|
implementation(deps.googlePlay.services.wallet)
|
||||||
|
implementation(deps.googlePlay.services)
|
||||||
|
implementation(deps.googlePlay.advertising)
|
||||||
coreLibraryDesugaring(deps.desugar)
|
coreLibraryDesugaring(deps.desugar)
|
||||||
implementation(deps.timber)
|
implementation(deps.timber)
|
||||||
implementation(deps.reKotlin)
|
implementation(deps.reKotlin)
|
||||||
|
|
@ -393,11 +398,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)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
<intent-filter android:autoVerify="true">
|
<intent-filter android:autoVerify="true">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
@ -337,6 +354,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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package com.tangem.tap.common.analytics.appsflyer
|
||||||
|
|
||||||
|
import com.appsflyer.deeplink.DeepLinkListener
|
||||||
|
import com.appsflyer.deeplink.DeepLinkResult
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class AppsFlyerDeepLinkListener @Inject constructor(
|
||||||
|
private val referralParamsHandler: AppsFlyerReferralParamsHandler,
|
||||||
|
) : DeepLinkListener {
|
||||||
|
|
||||||
|
override fun onDeepLinking(p0: DeepLinkResult) {
|
||||||
|
when (p0.status) {
|
||||||
|
DeepLinkResult.Status.FOUND -> {
|
||||||
|
referralParamsHandler.handle(deepLink = p0.deepLink)
|
||||||
|
}
|
||||||
|
DeepLinkResult.Status.NOT_FOUND -> {
|
||||||
|
Timber.i("No deep link found")
|
||||||
|
}
|
||||||
|
DeepLinkResult.Status.ERROR -> {
|
||||||
|
Timber.e("Deep link error: ${p0.error}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,64 +1,42 @@
|
||||||
package com.tangem.tap.common.analytics.handlers.appsflyer
|
package com.tangem.tap.common.analytics.handlers.appsflyer
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.appsflyer.AppsFlyerConversionListener
|
|
||||||
import com.appsflyer.AppsFlyerLib
|
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.appsflyer.TangemAFConversionListener
|
||||||
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,
|
||||||
|
tangemAFConversionListener: TangemAFConversionListener,
|
||||||
) : 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)
|
subscribeForDeepLink(appsFlyerDeepLinkListener)
|
||||||
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) {
|
init(apiKey, tangemAFConversionListener, context)
|
||||||
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 +55,32 @@ internal class AppsFlyerClient(
|
||||||
context,
|
context,
|
||||||
event,
|
event,
|
||||||
eventConverter.convertEventParams(params),
|
eventConverter.convertEventParams(params),
|
||||||
logEventListener,
|
LogEventListener,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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"
|
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)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
package com.tangem.tap.common.analytics.appsflyer
|
||||||
|
|
||||||
|
import com.appsflyer.deeplink.DeepLink
|
||||||
|
import com.appsflyer.deeplink.DeepLinkResult
|
||||||
|
import com.tangem.test.core.ProvideTestModels
|
||||||
|
import io.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 referralParamsHandler: AppsFlyerReferralParamsHandler = mockk()
|
||||||
|
private val listener = AppsFlyerDeepLinkListener(
|
||||||
|
referralParamsHandler = referralParamsHandler,
|
||||||
|
)
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
fun tearDown() {
|
||||||
|
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.shouldHandle) {
|
||||||
|
coVerify { referralParamsHandler.handle(deepLink = model.deepLinkResult.deepLink) }
|
||||||
|
} else {
|
||||||
|
coVerify(inverse = true) { referralParamsHandler.handle(deepLink = any()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun provideTestModels(): List<OnDeepLinkingModel> {
|
||||||
|
return listOf(
|
||||||
|
// DeepLinkResult.Status.NOT_FOUND
|
||||||
|
OnDeepLinkingModel(
|
||||||
|
deepLinkResult = DeepLinkResult(null, null),
|
||||||
|
shouldHandle = false,
|
||||||
|
),
|
||||||
|
// DeepLinkResult.Status.ERROR
|
||||||
|
OnDeepLinkingModel(
|
||||||
|
deepLinkResult = DeepLinkResult(null, DeepLinkResult.Error.NETWORK),
|
||||||
|
shouldHandle = false,
|
||||||
|
),
|
||||||
|
// DeepLinkResult.Status.FOUND
|
||||||
|
OnDeepLinkingModel(
|
||||||
|
deepLinkResult = createFoundDeepLink(
|
||||||
|
deepLinkValue = "deep_link_value",
|
||||||
|
refcode = "refcode",
|
||||||
|
campaign = "campaign",
|
||||||
|
),
|
||||||
|
shouldHandle = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class OnDeepLinkingModel(
|
||||||
|
val deepLinkResult: DeepLinkResult,
|
||||||
|
val shouldHandle: 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("deep_link_sub1") } returns refcode
|
||||||
|
every { this@mockk.getStringValue("deep_link_sub2") } returns campaign
|
||||||
|
}
|
||||||
|
|
||||||
|
return mockk<DeepLinkResult> {
|
||||||
|
every { this@mockk.status } returns DeepLinkResult.Status.FOUND
|
||||||
|
every { this@mockk.deepLink } returns deeplink
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<HandleDeepLinkModel> {
|
||||||
|
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<DeepLink> {
|
||||||
|
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<HandleParamsModel> {
|
||||||
|
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<String?, Any?>, val shouldStore: Boolean)
|
||||||
|
|
||||||
|
private companion object Companion {
|
||||||
|
const val SUCCESS_REFCODE = "valid_refcode"
|
||||||
|
const val SUCCESS_CAMPAIGN = "valid_campaign"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<OnConversionDataSuccessModel> {
|
||||||
|
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<String?, Any?>?,
|
||||||
|
val shouldHandle: Boolean,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -12,5 +12,7 @@ object DeeplinkConst {
|
||||||
const val DERIVATION_PATH_KEY = "derivation_path"
|
const val DERIVATION_PATH_KEY = "derivation_path"
|
||||||
const val TRANSACTION_ID_KEY = "transaction_id"
|
const val TRANSACTION_ID_KEY = "transaction_id"
|
||||||
const val PROMO_CODE_KEY = "promo_code"
|
const val PROMO_CODE_KEY = "promo_code"
|
||||||
|
const val REF_KEY = "ref"
|
||||||
|
const val CAMPAIGN_KEY = "campaign"
|
||||||
const val NAME_KEY = "name"
|
const val NAME_KEY = "name"
|
||||||
}
|
}
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "HOT_WALLET_VISIBLE",
|
"name": "HOT_WALLET_VISIBLE",
|
||||||
"version": "undefined"
|
"version": "5.32.1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "TANGEM_PAY_ENABLED",
|
"name": "TANGEM_PAY_ENABLED",
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ interface TangemTechApi {
|
||||||
@Body userTokens: UserTokensResponse,
|
@Body userTokens: UserTokensResponse,
|
||||||
): ApiResponse<Unit>
|
): ApiResponse<Unit>
|
||||||
|
|
||||||
|
// region Referral
|
||||||
/** Returns referral status by [walletId] */
|
/** Returns referral status by [walletId] */
|
||||||
@GET("v1/referral/{walletId}")
|
@GET("v1/referral/{walletId}")
|
||||||
suspend fun getReferralStatus(@Path("walletId") walletId: String): ApiResponse<ReferralResponse>
|
suspend fun getReferralStatus(@Path("walletId") walletId: String): ApiResponse<ReferralResponse>
|
||||||
|
|
@ -64,6 +65,10 @@ interface TangemTechApi {
|
||||||
@POST("v1/referral")
|
@POST("v1/referral")
|
||||||
suspend fun startReferral(@Body startReferralBody: StartReferralBody): ApiResponse<ReferralResponse>
|
suspend fun startReferral(@Body startReferralBody: StartReferralBody): ApiResponse<ReferralResponse>
|
||||||
|
|
||||||
|
@POST("v1/referral/bind-wallets-by-code")
|
||||||
|
suspend fun bindWalletsByReferralCode(@Body body: BindWalletsByReferralCodeBody): ApiResponse<Unit>
|
||||||
|
// endregion
|
||||||
|
|
||||||
@GET("v1/quotes")
|
@GET("v1/quotes")
|
||||||
suspend fun getQuotes(
|
suspend fun getQuotes(
|
||||||
@Query("currencyId") currencyId: String,
|
@Query("currencyId") currencyId: String,
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,21 @@ import com.tangem.datasource.api.tangemTech.models.CardInfoBody
|
||||||
import com.tangem.datasource.api.tangemTech.models.WalletIdBody
|
import com.tangem.datasource.api.tangemTech.models.WalletIdBody
|
||||||
import com.tangem.datasource.api.tangemTech.models.WalletType
|
import com.tangem.datasource.api.tangemTech.models.WalletType
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
|
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||||
|
|
||||||
object WalletIdBodyConverter {
|
object WalletIdBodyConverter {
|
||||||
|
|
||||||
fun convert(userWallet: UserWallet, publicKeys: Map<String, String>? = null): WalletIdBody {
|
fun convert(
|
||||||
|
userWallet: UserWallet,
|
||||||
|
conversionData: AppsFlyerConversionData? = null,
|
||||||
|
publicKeys: Map<String, String>? = null,
|
||||||
|
): WalletIdBody {
|
||||||
return WalletIdBody(
|
return WalletIdBody(
|
||||||
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,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.tangem.datasource.api.tangemTech.models
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class BindWalletsByReferralCodeBody(
|
||||||
|
@Json(name = "walletIds") val walletIds: List<String>,
|
||||||
|
@Json(name = "referralCode") val refcode: String,
|
||||||
|
@Json(name = "utmCampaign") val campaign: String? = null,
|
||||||
|
)
|
||||||
|
|
@ -17,6 +17,8 @@ data class UserTokensResponse(
|
||||||
@Json(name = "notifyStatus") val notifyStatus: Boolean? = null,
|
@Json(name = "notifyStatus") val notifyStatus: Boolean? = null,
|
||||||
@Json(name = "name") val walletName: String? = null,
|
@Json(name = "name") val walletName: String? = null,
|
||||||
@Json(name = "type") val walletType: WalletType? = null,
|
@Json(name = "type") val walletType: WalletType? = null,
|
||||||
|
@Json(name = "ref") val refcode: String? = null,
|
||||||
|
@Json(name = "campaign") val campaign: String? = null,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,6 @@ data class WalletIdBody(
|
||||||
@Json(name = "name") val name: String,
|
@Json(name = "name") val name: String,
|
||||||
@Json(name = "type") val walletType: WalletType? = null,
|
@Json(name = "type") val walletType: WalletType? = null,
|
||||||
@Json(name = "cards") val cards: List<CardInfoBody>? = null,
|
@Json(name = "cards") val cards: List<CardInfoBody>? = null,
|
||||||
|
@Json(name = "ref") val refcode: String? = null,
|
||||||
|
@Json(name = "campaign") val campaign: String? = null,
|
||||||
)
|
)
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.tangem.datasource.local.appsflyer
|
||||||
|
|
||||||
|
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||||
|
|
||||||
|
interface AppsFlyerConversionStore {
|
||||||
|
|
||||||
|
suspend fun get(): AppsFlyerConversionData?
|
||||||
|
|
||||||
|
suspend fun store(value: AppsFlyerConversionData)
|
||||||
|
|
||||||
|
suspend fun storeIfAbsent(value: AppsFlyerConversionData)
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun storeIfAbsent(value: AppsFlyerConversionData) {
|
||||||
|
Timber.i("Storing conversion data to store if absent: $value")
|
||||||
|
|
||||||
|
val dto = appPreferencesStore.getObjectSyncOrNull<ConversionDataDTO>(KEY)
|
||||||
|
|
||||||
|
if (dto == null) {
|
||||||
|
Timber.i("Conversion data is absent, storing $value")
|
||||||
|
appPreferencesStore.storeObject(KEY, ConversionDataConverter.convert(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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?,
|
||||||
|
)
|
||||||
|
|
@ -30,7 +30,11 @@ class WalletIdBodyConverterTest {
|
||||||
)
|
)
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
val result = WalletIdBodyConverter.convert(userWallet, publicKeys)
|
val result = WalletIdBodyConverter.convert(
|
||||||
|
userWallet = userWallet,
|
||||||
|
conversionData = null,
|
||||||
|
publicKeys = publicKeys,
|
||||||
|
)
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
assertThat(result).isEqualTo(
|
assertThat(result).isEqualTo(
|
||||||
|
|
@ -68,7 +72,11 @@ class WalletIdBodyConverterTest {
|
||||||
val publicKeys = emptyMap<String, String>()
|
val publicKeys = emptyMap<String, String>()
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
val result = WalletIdBodyConverter.convert(userWallet, publicKeys)
|
val result = WalletIdBodyConverter.convert(
|
||||||
|
userWallet = userWallet,
|
||||||
|
conversionData = null,
|
||||||
|
publicKeys = publicKeys,
|
||||||
|
)
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
assertThat(result).isEqualTo(
|
assertThat(result).isEqualTo(
|
||||||
|
|
|
||||||
|
|
@ -4,22 +4,18 @@ import com.tangem.data.account.fetcher.DefaultWalletAccountsFetcher.FetchResult
|
||||||
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
|
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
|
||||||
import com.tangem.data.common.currency.UserTokensResponseAccountIdEnricher
|
import com.tangem.data.common.currency.UserTokensResponseAccountIdEnricher
|
||||||
import com.tangem.data.common.currency.UserTokensSaver
|
import com.tangem.data.common.currency.UserTokensSaver
|
||||||
|
import com.tangem.data.common.wallet.WalletServerBinder
|
||||||
import com.tangem.datasource.api.common.response.ApiResponse
|
import com.tangem.datasource.api.common.response.ApiResponse
|
||||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||||
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code
|
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code
|
||||||
import com.tangem.datasource.api.common.response.ETAG_HEADER
|
import com.tangem.datasource.api.common.response.ETAG_HEADER
|
||||||
import com.tangem.datasource.api.common.response.isNetworkError
|
import com.tangem.datasource.api.common.response.isNetworkError
|
||||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
|
||||||
import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter
|
|
||||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||||
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
|
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
|
||||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
@ -27,26 +23,21 @@ import javax.inject.Singleton
|
||||||
/**
|
/**
|
||||||
* Handles errors that occur during the fetching of wallet accounts
|
* Handles errors that occur during the fetching of wallet accounts
|
||||||
*
|
*
|
||||||
* @property tangemTechApi API for network requests
|
|
||||||
* @property userWalletsStore provides access to user wallets storage
|
|
||||||
* @property userTokensSaver saves user tokens to the storage
|
* @property userTokensSaver saves user tokens to the storage
|
||||||
* @property userTokensResponseStore provides access to user token responses.
|
* @property userTokensResponseStore provides access to user token responses.
|
||||||
* @property defaultWalletAccountsResponseFactory creates [GetWalletAccountsResponse] from [UserTokensResponse]
|
* @property defaultWalletAccountsResponseFactory creates [GetWalletAccountsResponse] from [UserTokensResponse]
|
||||||
* @property dispatchers dispatchers
|
* @property walletServerBinder binds user wallets on the server
|
||||||
*
|
*
|
||||||
* @see DefaultWalletAccountsFetcher
|
* @see DefaultWalletAccountsFetcher
|
||||||
*
|
*
|
||||||
[REDACTED_AUTHOR]
|
[REDACTED_AUTHOR]
|
||||||
*/
|
*/
|
||||||
@Suppress("LongParameterList")
|
|
||||||
@Singleton
|
@Singleton
|
||||||
internal class FetchWalletAccountsErrorHandler @Inject constructor(
|
internal class FetchWalletAccountsErrorHandler @Inject constructor(
|
||||||
private val tangemTechApi: TangemTechApi,
|
|
||||||
private val userWalletsStore: UserWalletsStore,
|
|
||||||
private val userTokensSaver: UserTokensSaver,
|
private val userTokensSaver: UserTokensSaver,
|
||||||
private val userTokensResponseStore: UserTokensResponseStore,
|
private val userTokensResponseStore: UserTokensResponseStore,
|
||||||
private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory,
|
private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
private val walletServerBinder: WalletServerBinder,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -119,13 +110,7 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor(
|
||||||
|
|
||||||
*/
|
*/
|
||||||
private suspend fun createWallet(userWalletId: UserWalletId): String? {
|
private suspend fun createWallet(userWalletId: UserWalletId): String? {
|
||||||
val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId) ?: return null
|
val creationResponse = walletServerBinder.bind(userWalletId)
|
||||||
|
|
||||||
val creationResponse = withContext(dispatchers.io) {
|
|
||||||
tangemTechApi.createWallet(
|
|
||||||
body = WalletIdBodyConverter.convert(userWallet),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (creationResponse is ApiResponse.Success && creationResponse.code == Code.CREATED) {
|
return if (creationResponse is ApiResponse.Success && creationResponse.code == Code.CREATED) {
|
||||||
creationResponse.headers[ETAG_HEADER]?.firstOrNull()
|
creationResponse.headers[ETAG_HEADER]?.firstOrNull()
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,22 @@ import com.tangem.data.account.converter.createGetWalletAccountsResponse
|
||||||
import com.tangem.data.account.converter.createWalletAccountDTO
|
import com.tangem.data.account.converter.createWalletAccountDTO
|
||||||
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
|
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
|
||||||
import com.tangem.data.common.currency.UserTokensSaver
|
import com.tangem.data.common.currency.UserTokensSaver
|
||||||
|
import com.tangem.data.common.wallet.WalletServerBinder
|
||||||
import com.tangem.datasource.api.common.response.ApiResponse
|
import com.tangem.datasource.api.common.response.ApiResponse
|
||||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||||
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code
|
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code
|
||||||
import com.tangem.datasource.api.common.response.ETAG_HEADER
|
import com.tangem.datasource.api.common.response.ETAG_HEADER
|
||||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||||
import com.tangem.datasource.api.tangemTech.models.WalletIdBody
|
|
||||||
import com.tangem.datasource.api.tangemTech.models.WalletType
|
|
||||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||||
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
|
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
|
||||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
import io.mockk.clearMocks
|
||||||
import io.mockk.*
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
@ -33,18 +32,16 @@ import org.junit.jupiter.api.TestInstance
|
||||||
class FetchWalletAccountsErrorHandlerTest {
|
class FetchWalletAccountsErrorHandlerTest {
|
||||||
|
|
||||||
private val tangemTechApi: TangemTechApi = mockk()
|
private val tangemTechApi: TangemTechApi = mockk()
|
||||||
private val userWalletsStore: UserWalletsStore = mockk()
|
private val walletServerBinder: WalletServerBinder = mockk()
|
||||||
private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true)
|
private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true)
|
||||||
private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true)
|
private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true)
|
||||||
private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory = mockk()
|
private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory = mockk()
|
||||||
|
|
||||||
private val handler = FetchWalletAccountsErrorHandler(
|
private val handler = FetchWalletAccountsErrorHandler(
|
||||||
tangemTechApi = tangemTechApi,
|
walletServerBinder = walletServerBinder,
|
||||||
userWalletsStore = userWalletsStore,
|
|
||||||
userTokensSaver = userTokensSaver,
|
userTokensSaver = userTokensSaver,
|
||||||
userTokensResponseStore = userTokensResponseStore,
|
userTokensResponseStore = userTokensResponseStore,
|
||||||
defaultWalletAccountsResponseFactory = defaultWalletAccountsResponseFactory,
|
defaultWalletAccountsResponseFactory = defaultWalletAccountsResponseFactory,
|
||||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private val pushWalletAccounts: suspend (List<WalletAccountDTO>, String) -> GetWalletAccountsResponse =
|
private val pushWalletAccounts: suspend (List<WalletAccountDTO>, String) -> GetWalletAccountsResponse =
|
||||||
|
|
@ -55,7 +52,7 @@ class FetchWalletAccountsErrorHandlerTest {
|
||||||
fun setupEach() {
|
fun setupEach() {
|
||||||
clearMocks(
|
clearMocks(
|
||||||
tangemTechApi,
|
tangemTechApi,
|
||||||
userWalletsStore,
|
walletServerBinder,
|
||||||
userTokensSaver,
|
userTokensSaver,
|
||||||
userTokensResponseStore,
|
userTokensResponseStore,
|
||||||
defaultWalletAccountsResponseFactory,
|
defaultWalletAccountsResponseFactory,
|
||||||
|
|
@ -112,22 +109,7 @@ class FetchWalletAccountsErrorHandlerTest {
|
||||||
code = Code.CREATED,
|
code = Code.CREATED,
|
||||||
)
|
)
|
||||||
|
|
||||||
val walletName = "Wallet"
|
coEvery { walletServerBinder.bind(userWalletId) } returns apiResponse
|
||||||
val userWallet = mockk<UserWallet.Cold> {
|
|
||||||
every { this@mockk.walletId } returns userWalletId
|
|
||||||
every { this@mockk.name } returns walletName
|
|
||||||
}
|
|
||||||
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
|
|
||||||
|
|
||||||
coEvery {
|
|
||||||
tangemTechApi.createWallet(
|
|
||||||
WalletIdBody(
|
|
||||||
walletId = userWalletId.stringValue,
|
|
||||||
name = walletName,
|
|
||||||
walletType = WalletType.COLD,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} returns apiResponse
|
|
||||||
coEvery { pushWalletAccounts(listOf(accountDTO), eTagValue) } returns savedAccountsResponse
|
coEvery { pushWalletAccounts(listOf(accountDTO), eTagValue) } returns savedAccountsResponse
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -142,13 +124,7 @@ class FetchWalletAccountsErrorHandlerTest {
|
||||||
// Assert
|
// Assert
|
||||||
coVerify {
|
coVerify {
|
||||||
userTokensSaver.pushWithRetryer(userWalletId, response = savedAccountsResponse.toUserTokensResponse())
|
userTokensSaver.pushWithRetryer(userWalletId, response = savedAccountsResponse.toUserTokensResponse())
|
||||||
tangemTechApi.createWallet(
|
walletServerBinder.bind(userWalletId)
|
||||||
WalletIdBody(
|
|
||||||
walletId = userWalletId.stringValue,
|
|
||||||
name = walletName,
|
|
||||||
walletType = WalletType.COLD,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
pushWalletAccounts(listOf(accountDTO), eTagValue)
|
pushWalletAccounts(listOf(accountDTO), eTagValue)
|
||||||
storeWalletAccounts(userWalletId, savedAccountsResponse)
|
storeWalletAccounts(userWalletId, savedAccountsResponse)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,14 @@ package com.tangem.data.common.currency
|
||||||
|
|
||||||
import com.tangem.data.common.api.safeApiCall
|
import com.tangem.data.common.api.safeApiCall
|
||||||
import com.tangem.data.common.tokens.UserTokensBackwardCompatibility
|
import com.tangem.data.common.tokens.UserTokensBackwardCompatibility
|
||||||
|
import com.tangem.data.common.wallet.WalletServerBinder
|
||||||
import com.tangem.datasource.api.common.response.ApiResponse
|
import com.tangem.datasource.api.common.response.ApiResponse
|
||||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||||
import com.tangem.datasource.api.common.response.isNetworkError
|
import com.tangem.datasource.api.common.response.isNetworkError
|
||||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||||
import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter
|
|
||||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||||
import com.tangem.datasource.api.tangemTech.models.WalletType
|
import com.tangem.datasource.api.tangemTech.models.WalletType
|
||||||
|
import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore
|
||||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||||
|
|
@ -27,6 +28,8 @@ class UserTokensSaver(
|
||||||
private val userTokensResponseStore: UserTokensResponseStore,
|
private val userTokensResponseStore: UserTokensResponseStore,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
private val addressesEnricher: UserTokensResponseAddressesEnricher,
|
private val addressesEnricher: UserTokensResponseAddressesEnricher,
|
||||||
|
private val walletServerBinder: WalletServerBinder,
|
||||||
|
private val appsFlyerConversionStore: AppsFlyerConversionStore,
|
||||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||||
private val pushTokensRetryerPool: RetryerPool,
|
private val pushTokensRetryerPool: RetryerPool,
|
||||||
) {
|
) {
|
||||||
|
|
@ -69,9 +72,13 @@ class UserTokensSaver(
|
||||||
|
|
||||||
pushNew(userWallet = userWallet, response = enrichedResponse, onFailSend = onFailSend)
|
pushNew(userWallet = userWallet, response = enrichedResponse, onFailSend = onFailSend)
|
||||||
} else {
|
} else {
|
||||||
|
val conversionData = appsFlyerConversionStore.get()
|
||||||
|
|
||||||
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)
|
||||||
|
|
@ -114,7 +121,7 @@ class UserTokensSaver(
|
||||||
apiResponse.cause.isNetworkError(ApiResponseError.HttpException.Code.NOT_FOUND)
|
apiResponse.cause.isNetworkError(ApiResponseError.HttpException.Code.NOT_FOUND)
|
||||||
|
|
||||||
if (isWalletNotFound) {
|
if (isWalletNotFound) {
|
||||||
tangemTechApi.createWallet(body = WalletIdBodyConverter.convert(userWallet)).bind()
|
walletServerBinder.bind(userWallet).bind()
|
||||||
|
|
||||||
tangemTechApi.saveTokens(
|
tangemTechApi.saveTokens(
|
||||||
userId = userWallet.walletId.stringValue,
|
userId = userWallet.walletId.stringValue,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,10 @@ import com.tangem.data.common.cache.etag.ETagsStore
|
||||||
import com.tangem.data.common.currency.*
|
import com.tangem.data.common.currency.*
|
||||||
import com.tangem.data.common.quote.DefaultQuotesFetcher
|
import com.tangem.data.common.quote.DefaultQuotesFetcher
|
||||||
import com.tangem.data.common.quote.QuotesFetcher
|
import com.tangem.data.common.quote.QuotesFetcher
|
||||||
|
import com.tangem.data.common.wallet.DefaultWalletServerBinder
|
||||||
|
import com.tangem.data.common.wallet.WalletServerBinder
|
||||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||||
|
import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||||
|
|
@ -72,6 +75,8 @@ internal object DataCommonModule {
|
||||||
userTokensResponseStore: UserTokensResponseStore,
|
userTokensResponseStore: UserTokensResponseStore,
|
||||||
dispatchers: CoroutineDispatcherProvider,
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
addressesEnricher: UserTokensResponseAddressesEnricher,
|
addressesEnricher: UserTokensResponseAddressesEnricher,
|
||||||
|
walletServerBinder: WalletServerBinder,
|
||||||
|
appsFlyerConversionStore: AppsFlyerConversionStore,
|
||||||
accountsFeatureToggles: AccountsFeatureToggles,
|
accountsFeatureToggles: AccountsFeatureToggles,
|
||||||
): UserTokensSaver {
|
): UserTokensSaver {
|
||||||
return UserTokensSaver(
|
return UserTokensSaver(
|
||||||
|
|
@ -84,6 +89,8 @@ internal object DataCommonModule {
|
||||||
pushTokensRetryerPool = RetryerPool(
|
pushTokensRetryerPool = RetryerPool(
|
||||||
coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default),
|
coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default),
|
||||||
),
|
),
|
||||||
|
walletServerBinder = walletServerBinder,
|
||||||
|
appsFlyerConversionStore = appsFlyerConversionStore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,4 +105,20 @@ internal object DataCommonModule {
|
||||||
fun provideETagsStore(appPreferencesStore: AppPreferencesStore): ETagsStore {
|
fun provideETagsStore(appPreferencesStore: AppPreferencesStore): ETagsStore {
|
||||||
return DefaultETagsStore(appPreferencesStore = appPreferencesStore)
|
return DefaultETagsStore(appPreferencesStore = appPreferencesStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideWalletServerBinder(
|
||||||
|
userWalletsStore: UserWalletsStore,
|
||||||
|
appsFlyerConversionStore: AppsFlyerConversionStore,
|
||||||
|
tangemTechApi: TangemTechApi,
|
||||||
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
|
): WalletServerBinder {
|
||||||
|
return DefaultWalletServerBinder(
|
||||||
|
userWalletsStore = userWalletsStore,
|
||||||
|
appsFlyerConversionStore = appsFlyerConversionStore,
|
||||||
|
tangemTechApi = tangemTechApi,
|
||||||
|
dispatchers = dispatchers,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.tangem.data.common.wallet
|
||||||
|
|
||||||
|
import com.tangem.datasource.api.common.response.ApiResponse
|
||||||
|
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||||
|
import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter
|
||||||
|
import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore
|
||||||
|
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||||
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
internal class DefaultWalletServerBinder(
|
||||||
|
private val userWalletsStore: UserWalletsStore,
|
||||||
|
private val appsFlyerConversionStore: AppsFlyerConversionStore,
|
||||||
|
private val tangemTechApi: TangemTechApi,
|
||||||
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
|
) : WalletServerBinder {
|
||||||
|
|
||||||
|
override suspend fun bind(userWalletId: UserWalletId): ApiResponse<Unit>? {
|
||||||
|
val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId) ?: return null
|
||||||
|
|
||||||
|
return bind(userWallet)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun bind(userWallet: UserWallet): ApiResponse<Unit> {
|
||||||
|
val conversionData = appsFlyerConversionStore.get()
|
||||||
|
|
||||||
|
return withContext(dispatchers.io) {
|
||||||
|
tangemTechApi.createWallet(
|
||||||
|
body = WalletIdBodyConverter.convert(userWallet, conversionData),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.tangem.data.common.wallet
|
||||||
|
|
||||||
|
import com.tangem.datasource.api.common.response.ApiResponse
|
||||||
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
|
||||||
|
/**
|
||||||
|
|
||||||
|
*
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
interface WalletServerBinder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds a wallet to the server using its [userWalletId].
|
||||||
|
* Returns an [ApiResponse] indicating the result of the binding operation,
|
||||||
|
* or null if the binding was not performed.
|
||||||
|
*/
|
||||||
|
suspend fun bind(userWalletId: UserWalletId): ApiResponse<Unit>?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds a wallet to the server using the provided [userWallet].
|
||||||
|
* Returns an [ApiResponse] indicating the result of the binding operation.
|
||||||
|
*/
|
||||||
|
suspend fun bind(userWallet: UserWallet): ApiResponse<Unit>
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
package com.tangem.data.common.currency
|
package com.tangem.data.common.currency
|
||||||
|
|
||||||
|
import com.tangem.data.common.wallet.WalletServerBinder
|
||||||
import com.tangem.datasource.api.common.response.ApiResponse
|
import com.tangem.datasource.api.common.response.ApiResponse
|
||||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||||
import com.tangem.datasource.api.tangemTech.models.WalletType
|
import com.tangem.datasource.api.tangemTech.models.WalletType
|
||||||
|
import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore
|
||||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||||
|
|
@ -27,6 +29,8 @@ class UserTokensSaverTest {
|
||||||
private val accountsFeatureToggles = mockk<AccountsFeatureToggles> {
|
private val accountsFeatureToggles = mockk<AccountsFeatureToggles> {
|
||||||
every { this@mockk.isFeatureEnabled } returns true
|
every { this@mockk.isFeatureEnabled } returns true
|
||||||
}
|
}
|
||||||
|
private val walletServerBinder: WalletServerBinder = mockk()
|
||||||
|
private val appsFlyerConversionStore: AppsFlyerConversionStore = mockk()
|
||||||
|
|
||||||
private val userTokensSaver: UserTokensSaver = UserTokensSaver(
|
private val userTokensSaver: UserTokensSaver = UserTokensSaver(
|
||||||
tangemTechApi = tangemTechApi,
|
tangemTechApi = tangemTechApi,
|
||||||
|
|
@ -34,13 +38,21 @@ class UserTokensSaverTest {
|
||||||
userTokensResponseStore = userTokensResponseStore,
|
userTokensResponseStore = userTokensResponseStore,
|
||||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||||
addressesEnricher = enricher,
|
addressesEnricher = enricher,
|
||||||
|
walletServerBinder = walletServerBinder,
|
||||||
|
appsFlyerConversionStore = appsFlyerConversionStore,
|
||||||
accountsFeatureToggles = accountsFeatureToggles,
|
accountsFeatureToggles = accountsFeatureToggles,
|
||||||
pushTokensRetryerPool = mockk(),
|
pushTokensRetryerPool = mockk(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun resetMocks() {
|
fun resetMocks() {
|
||||||
clearMocks(tangemTechApi, userWalletsStore, userTokensResponseStore, enricher)
|
clearMocks(
|
||||||
|
tangemTechApi,
|
||||||
|
userWalletsStore,
|
||||||
|
userTokensResponseStore,
|
||||||
|
enricher,
|
||||||
|
walletServerBinder,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -109,7 +121,6 @@ class UserTokensSaverTest {
|
||||||
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
|
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
|
||||||
coEvery { enricher(userWalletId, response) } returns enrichedResponse
|
coEvery { enricher(userWalletId, response) } returns enrichedResponse
|
||||||
coEvery { tangemTechApi.saveTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse<Unit>
|
coEvery { tangemTechApi.saveTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse<Unit>
|
||||||
coEvery { tangemTechApi.createWallet(body = any()) } returns ApiResponse.Error(error) as ApiResponse<Unit>
|
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
userTokensSaver.push(
|
userTokensSaver.push(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
package com.tangem.data.common.wallet
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth
|
||||||
|
import com.tangem.datasource.api.common.response.ApiResponse
|
||||||
|
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||||
|
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||||
|
import com.tangem.datasource.api.tangemTech.models.WalletIdBody
|
||||||
|
import com.tangem.datasource.api.tangemTech.models.WalletType
|
||||||
|
import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore
|
||||||
|
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||||
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||||
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||||
|
import io.mockk.*
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.AfterEach
|
||||||
|
import org.junit.jupiter.api.Nested
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.TestInstance
|
||||||
|
|
||||||
|
/**
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
internal class DefaultWalletServerBinderTest {
|
||||||
|
|
||||||
|
private val userWalletsStore: UserWalletsStore = mockk()
|
||||||
|
private val appsFlyerConversionStore: AppsFlyerConversionStore = mockk()
|
||||||
|
private val tangemTechApi: TangemTechApi = mockk()
|
||||||
|
private val dispatchers: CoroutineDispatcherProvider = TestingCoroutineDispatcherProvider()
|
||||||
|
|
||||||
|
private val binder = DefaultWalletServerBinder(
|
||||||
|
userWalletsStore = userWalletsStore,
|
||||||
|
appsFlyerConversionStore = appsFlyerConversionStore,
|
||||||
|
tangemTechApi = tangemTechApi,
|
||||||
|
dispatchers = dispatchers
|
||||||
|
)
|
||||||
|
|
||||||
|
private val userWalletId = UserWalletId("011")
|
||||||
|
private val userWallet = mockk<UserWallet.Cold> {
|
||||||
|
every { this@mockk.walletId } returns userWalletId
|
||||||
|
every { this@mockk.name } returns "Wallet"
|
||||||
|
}
|
||||||
|
private val conversionData = AppsFlyerConversionData(refcode = "refcode", campaign = "campaign")
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
fun tearDown() {
|
||||||
|
clearMocks(userWalletsStore, appsFlyerConversionStore, tangemTechApi)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
inner class BindUserWalletId {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `bind successfully`() = runTest {
|
||||||
|
val requestBody = WalletIdBody(
|
||||||
|
walletId = userWalletId.stringValue,
|
||||||
|
name = "Wallet",
|
||||||
|
walletType = WalletType.COLD,
|
||||||
|
refcode = "refcode",
|
||||||
|
campaign = "campaign",
|
||||||
|
)
|
||||||
|
val apiResponse = ApiResponse.Success(Unit)
|
||||||
|
|
||||||
|
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
|
||||||
|
coEvery { appsFlyerConversionStore.get() } returns conversionData
|
||||||
|
coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse
|
||||||
|
|
||||||
|
val actual = binder.bind(userWalletId = userWalletId)
|
||||||
|
|
||||||
|
Truth.assertThat(actual).isEqualTo(apiResponse)
|
||||||
|
|
||||||
|
coVerifyOrder {
|
||||||
|
userWalletsStore.getSyncOrNull(userWalletId)
|
||||||
|
appsFlyerConversionStore.get()
|
||||||
|
tangemTechApi.createWallet(requestBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `bind will skipped if userWalletsStore returns null`() = runTest {
|
||||||
|
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns null
|
||||||
|
|
||||||
|
val actual = binder.bind(userWalletId = userWalletId)
|
||||||
|
|
||||||
|
Truth.assertThat(actual).isEqualTo(null)
|
||||||
|
|
||||||
|
coVerifyOrder {
|
||||||
|
userWalletsStore.getSyncOrNull(userWalletId)
|
||||||
|
}
|
||||||
|
|
||||||
|
coVerify(inverse = true) {
|
||||||
|
appsFlyerConversionStore.get()
|
||||||
|
tangemTechApi.createWallet(any())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `bind successfully if appsFlyerConversionStore returns null`() = runTest {
|
||||||
|
val requestBody = WalletIdBody(
|
||||||
|
walletId = userWalletId.stringValue,
|
||||||
|
name = "Wallet",
|
||||||
|
walletType = WalletType.COLD,
|
||||||
|
)
|
||||||
|
val apiResponse = ApiResponse.Success(Unit)
|
||||||
|
|
||||||
|
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
|
||||||
|
coEvery { appsFlyerConversionStore.get() } returns null
|
||||||
|
coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse
|
||||||
|
|
||||||
|
val actual = binder.bind(userWalletId = userWalletId)
|
||||||
|
|
||||||
|
Truth.assertThat(actual).isEqualTo(apiResponse)
|
||||||
|
|
||||||
|
coVerifyOrder {
|
||||||
|
userWalletsStore.getSyncOrNull(userWalletId)
|
||||||
|
appsFlyerConversionStore.get()
|
||||||
|
tangemTechApi.createWallet(requestBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `bind returns error if request is failed`() = runTest {
|
||||||
|
val requestBody = WalletIdBody(
|
||||||
|
walletId = userWalletId.stringValue,
|
||||||
|
name = "Wallet",
|
||||||
|
walletType = WalletType.COLD,
|
||||||
|
refcode = "refcode",
|
||||||
|
campaign = "campaign",
|
||||||
|
)
|
||||||
|
val apiResponse = ApiResponse.Error(ApiResponseError.TimeoutException()) as ApiResponse<Unit>
|
||||||
|
|
||||||
|
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
|
||||||
|
coEvery { appsFlyerConversionStore.get() } returns conversionData
|
||||||
|
coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse
|
||||||
|
|
||||||
|
val actual = binder.bind(userWalletId = userWalletId)
|
||||||
|
|
||||||
|
Truth.assertThat(actual).isEqualTo(apiResponse)
|
||||||
|
|
||||||
|
coVerifyOrder {
|
||||||
|
userWalletsStore.getSyncOrNull(userWalletId)
|
||||||
|
appsFlyerConversionStore.get()
|
||||||
|
tangemTechApi.createWallet(requestBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
inner class BindUserWallet {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `bind successfully`() = runTest {
|
||||||
|
val requestBody = WalletIdBody(
|
||||||
|
walletId = userWalletId.stringValue,
|
||||||
|
name = "Wallet",
|
||||||
|
walletType = WalletType.COLD,
|
||||||
|
refcode = "refcode",
|
||||||
|
campaign = "campaign",
|
||||||
|
)
|
||||||
|
val apiResponse = ApiResponse.Success(Unit)
|
||||||
|
|
||||||
|
coEvery { appsFlyerConversionStore.get() } returns conversionData
|
||||||
|
coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse
|
||||||
|
|
||||||
|
val actual = binder.bind(userWallet = userWallet)
|
||||||
|
|
||||||
|
Truth.assertThat(actual).isEqualTo(apiResponse)
|
||||||
|
|
||||||
|
coVerifyOrder {
|
||||||
|
appsFlyerConversionStore.get()
|
||||||
|
tangemTechApi.createWallet(requestBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `bind successfully if appsFlyerConversionStore returns null`() = runTest {
|
||||||
|
val requestBody = WalletIdBody(
|
||||||
|
walletId = userWalletId.stringValue,
|
||||||
|
name = "Wallet",
|
||||||
|
walletType = WalletType.COLD,
|
||||||
|
)
|
||||||
|
val apiResponse = ApiResponse.Success(Unit)
|
||||||
|
|
||||||
|
coEvery { appsFlyerConversionStore.get() } returns null
|
||||||
|
coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse
|
||||||
|
|
||||||
|
val actual = binder.bind(userWallet = userWallet)
|
||||||
|
|
||||||
|
Truth.assertThat(actual).isEqualTo(apiResponse)
|
||||||
|
|
||||||
|
coVerifyOrder {
|
||||||
|
appsFlyerConversionStore.get()
|
||||||
|
tangemTechApi.createWallet(requestBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `bind returns error if request is failed`() = runTest {
|
||||||
|
val requestBody = WalletIdBody(
|
||||||
|
walletId = userWalletId.stringValue,
|
||||||
|
name = "Wallet",
|
||||||
|
walletType = WalletType.COLD,
|
||||||
|
refcode = "refcode",
|
||||||
|
campaign = "campaign",
|
||||||
|
)
|
||||||
|
val apiResponse = ApiResponse.Error(ApiResponseError.TimeoutException()) as ApiResponse<Unit>
|
||||||
|
|
||||||
|
coEvery { appsFlyerConversionStore.get() } returns conversionData
|
||||||
|
coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse
|
||||||
|
|
||||||
|
val actual = binder.bind(userWallet = userWallet)
|
||||||
|
|
||||||
|
Truth.assertThat(actual).isEqualTo(apiResponse)
|
||||||
|
|
||||||
|
coVerifyOrder {
|
||||||
|
appsFlyerConversionStore.get()
|
||||||
|
tangemTechApi.createWallet(requestBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
package com.tangem.data.wallets
|
||||||
|
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import com.tangem.datasource.api.common.response.ApiResponse
|
||||||
|
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||||
|
import com.tangem.datasource.api.tangemTech.models.BindWalletsByReferralCodeBody
|
||||||
|
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.datasource.local.userwallet.UserWalletsStore
|
||||||
|
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||||
|
import com.tangem.domain.wallets.repository.WalletsPromoRepository
|
||||||
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
internal class DefaultWalletsPromoRepository(
|
||||||
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
|
private val tangemTechApi: TangemTechApi,
|
||||||
|
private val userWalletsStore: UserWalletsStore,
|
||||||
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
|
) : WalletsPromoRepository {
|
||||||
|
|
||||||
|
override suspend fun bindRefcodeWithWallets(conversionData: AppsFlyerConversionData) = withContext(dispatchers.io) {
|
||||||
|
val savedBindingData = getSavedBindingData()
|
||||||
|
|
||||||
|
when {
|
||||||
|
savedBindingData == null -> {
|
||||||
|
// first time binding
|
||||||
|
saveBindingData(refcode = conversionData.refcode, campaign = conversionData.campaign)
|
||||||
|
bind(refcode = conversionData.refcode, campaign = conversionData.campaign)
|
||||||
|
}
|
||||||
|
!savedBindingData.isDone -> {
|
||||||
|
// try again
|
||||||
|
bind(refcode = savedBindingData.refcode, campaign = savedBindingData.campaign)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun retryBindRefcodeWithWallets() {
|
||||||
|
val savedBindingData = getSavedBindingData()
|
||||||
|
|
||||||
|
if (savedBindingData != null) {
|
||||||
|
bind(refcode = savedBindingData.refcode, campaign = savedBindingData.campaign)
|
||||||
|
} else {
|
||||||
|
Timber.i("retryBindRefcodeWithWallets: Binding data isn't required")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getSavedBindingData(): ReferralWalletsBindingData? {
|
||||||
|
return appPreferencesStore.getObjectSyncOrNull(key = REFERRAL_WALLETS_BINDING_DATA_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun saveBindingData(refcode: String, campaign: String?) {
|
||||||
|
val bindingData = ReferralWalletsBindingData(refcode = refcode, campaign = campaign, isDone = false)
|
||||||
|
|
||||||
|
appPreferencesStore.storeObject(key = REFERRAL_WALLETS_BINDING_DATA_KEY, value = bindingData)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun bind(refcode: String, campaign: String?) {
|
||||||
|
val walletIds = userWalletsStore.userWalletsSync.map { it.walletId.stringValue }
|
||||||
|
|
||||||
|
val result = tangemTechApi.bindWalletsByReferralCode(
|
||||||
|
body = BindWalletsByReferralCodeBody(walletIds = walletIds, refcode = refcode, campaign = campaign),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result is ApiResponse.Success) {
|
||||||
|
val successData = ReferralWalletsBindingData(refcode = refcode, campaign = campaign, isDone = true)
|
||||||
|
|
||||||
|
appPreferencesStore.storeObject(key = REFERRAL_WALLETS_BINDING_DATA_KEY, value = successData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
private data class ReferralWalletsBindingData(
|
||||||
|
@Json(name = "refcode") val refcode: String,
|
||||||
|
@Json(name = "campaign") val campaign: String?,
|
||||||
|
@Json(name = "done") val isDone: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
val REFERRAL_WALLETS_BINDING_DATA_KEY by lazy { stringPreferencesKey(name = "referralWalletsBindingData") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package com.tangem.data.wallets
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
import arrow.core.left
|
import arrow.core.left
|
||||||
import arrow.core.right
|
import arrow.core.right
|
||||||
|
import com.tangem.data.common.wallet.WalletServerBinder
|
||||||
import com.tangem.data.wallets.converters.UserWalletRemoteInfoConverter
|
import com.tangem.data.wallets.converters.UserWalletRemoteInfoConverter
|
||||||
import com.tangem.datasource.api.common.AuthProvider
|
import com.tangem.datasource.api.common.AuthProvider
|
||||||
import com.tangem.datasource.api.common.response.ApiResponse
|
import com.tangem.datasource.api.common.response.ApiResponse
|
||||||
|
|
@ -14,6 +15,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||||
import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter
|
import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter
|
||||||
import com.tangem.datasource.api.tangemTech.models.*
|
import com.tangem.datasource.api.tangemTech.models.*
|
||||||
import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO.Status
|
import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO.Status
|
||||||
|
import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore
|
||||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||||
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
|
||||||
|
|
@ -46,6 +48,8 @@ internal class DefaultWalletsRepository(
|
||||||
private val seedPhraseNotificationVisibilityStore: RuntimeStateStore<SeedPhraseNotificationsStatuses>,
|
private val seedPhraseNotificationVisibilityStore: RuntimeStateStore<SeedPhraseNotificationsStatuses>,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
private val authProvider: AuthProvider,
|
private val authProvider: AuthProvider,
|
||||||
|
private val walletServerBinder: WalletServerBinder,
|
||||||
|
private val appsFlyerConversionStore: AppsFlyerConversionStore,
|
||||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||||
private val moshi: com.squareup.moshi.Moshi,
|
private val moshi: com.squareup.moshi.Moshi,
|
||||||
) : WalletsRepository {
|
) : WalletsRepository {
|
||||||
|
|
@ -250,13 +254,7 @@ internal class DefaultWalletsRepository(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun createWallet(userWalletId: UserWalletId) {
|
override suspend fun createWallet(userWalletId: UserWalletId) {
|
||||||
runCatching(dispatchers.io) {
|
walletServerBinder.bind(userWalletId)
|
||||||
val userWallet = userWalletsStore.getSyncStrict(key = userWalletId)
|
|
||||||
|
|
||||||
tangemTechApi.createWallet(
|
|
||||||
body = WalletIdBodyConverter.convert(userWallet = userWallet),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun rejectSeedPhraseSecondNotification(userWalletId: UserWalletId) {
|
override suspend fun rejectSeedPhraseSecondNotification(userWalletId: UserWalletId) {
|
||||||
|
|
@ -413,10 +411,12 @@ internal class DefaultWalletsRepository(
|
||||||
associateApplicationIdWithWallets().getOrThrow()
|
associateApplicationIdWithWallets().getOrThrow()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
val conversionData = appsFlyerConversionStore.get()
|
||||||
val publicKeys = authProvider.getCardsPublicKeys()
|
val publicKeys = authProvider.getCardsPublicKeys()
|
||||||
val walletsBody = wallets.map { userWallet ->
|
val walletsBody = wallets.map { userWallet ->
|
||||||
WalletIdBodyConverter.convert(
|
WalletIdBodyConverter.convert(
|
||||||
userWallet = userWallet,
|
userWallet = userWallet,
|
||||||
|
conversionData = conversionData,
|
||||||
publicKeys = if (userWallet is UserWallet.Cold) {
|
publicKeys = if (userWallet is UserWallet.Cold) {
|
||||||
publicKeys.filterKeys {
|
publicKeys.filterKeys {
|
||||||
userWallet.cardsInWallet.contains(it)
|
userWallet.cardsInWallet.contains(it)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package com.tangem.data.wallets.di
|
package com.tangem.data.wallets.di
|
||||||
|
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
|
import com.tangem.data.common.wallet.WalletServerBinder
|
||||||
import com.tangem.data.wallets.DefaultWalletNamesMigrationRepository
|
import com.tangem.data.wallets.DefaultWalletNamesMigrationRepository
|
||||||
|
import com.tangem.data.wallets.DefaultWalletsPromoRepository
|
||||||
import com.tangem.data.wallets.DefaultWalletsRepository
|
import com.tangem.data.wallets.DefaultWalletsRepository
|
||||||
import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository
|
import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository
|
||||||
import com.tangem.data.wallets.derivations.DefaultDerivationsRepository
|
import com.tangem.data.wallets.derivations.DefaultDerivationsRepository
|
||||||
|
|
@ -10,6 +12,7 @@ import com.tangem.data.wallets.hot.DefaultHotWalletAccessCodeAttemptsRepository
|
||||||
import com.tangem.datasource.api.common.AuthProvider
|
import com.tangem.datasource.api.common.AuthProvider
|
||||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||||
import com.tangem.datasource.di.NetworkMoshi
|
import com.tangem.datasource.di.NetworkMoshi
|
||||||
|
import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore
|
||||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||||
|
|
@ -19,6 +22,7 @@ import com.tangem.domain.wallets.derivations.DerivationsRepository
|
||||||
import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository
|
import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository
|
||||||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
|
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
|
||||||
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
|
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
|
||||||
|
import com.tangem.domain.wallets.repository.WalletsPromoRepository
|
||||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
|
|
@ -40,6 +44,8 @@ internal object WalletsDataModule {
|
||||||
userWalletsStore: UserWalletsStore,
|
userWalletsStore: UserWalletsStore,
|
||||||
dispatchers: CoroutineDispatcherProvider,
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
authProvider: AuthProvider,
|
authProvider: AuthProvider,
|
||||||
|
walletServerBinder: WalletServerBinder,
|
||||||
|
appsFlyerConversionStore: AppsFlyerConversionStore,
|
||||||
accountsFeatureToggles: AccountsFeatureToggles,
|
accountsFeatureToggles: AccountsFeatureToggles,
|
||||||
@NetworkMoshi moshi: Moshi,
|
@NetworkMoshi moshi: Moshi,
|
||||||
): WalletsRepository {
|
): WalletsRepository {
|
||||||
|
|
@ -50,6 +56,8 @@ internal object WalletsDataModule {
|
||||||
seedPhraseNotificationVisibilityStore = RuntimeStateStore(defaultValue = emptyMap()),
|
seedPhraseNotificationVisibilityStore = RuntimeStateStore(defaultValue = emptyMap()),
|
||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
authProvider = authProvider,
|
authProvider = authProvider,
|
||||||
|
walletServerBinder = walletServerBinder,
|
||||||
|
appsFlyerConversionStore = appsFlyerConversionStore,
|
||||||
accountsFeatureToggles = accountsFeatureToggles,
|
accountsFeatureToggles = accountsFeatureToggles,
|
||||||
moshi = moshi,
|
moshi = moshi,
|
||||||
)
|
)
|
||||||
|
|
@ -60,6 +68,22 @@ internal object WalletsDataModule {
|
||||||
fun provideMigrateNamesRepository(appPreferencesStore: AppPreferencesStore): WalletNamesMigrationRepository {
|
fun provideMigrateNamesRepository(appPreferencesStore: AppPreferencesStore): WalletNamesMigrationRepository {
|
||||||
return DefaultWalletNamesMigrationRepository(appPreferencesStore)
|
return DefaultWalletNamesMigrationRepository(appPreferencesStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideWalletsPromoRepository(
|
||||||
|
appPreferencesStore: AppPreferencesStore,
|
||||||
|
tangemTechApi: TangemTechApi,
|
||||||
|
userWalletsStore: UserWalletsStore,
|
||||||
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
|
): WalletsPromoRepository {
|
||||||
|
return DefaultWalletsPromoRepository(
|
||||||
|
appPreferencesStore = appPreferencesStore,
|
||||||
|
tangemTechApi = tangemTechApi,
|
||||||
|
userWalletsStore = userWalletsStore,
|
||||||
|
dispatchers = dispatchers,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import androidx.datastore.core.DataStore
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
|
import com.tangem.data.common.wallet.WalletServerBinder
|
||||||
import com.tangem.datasource.api.common.AuthProvider
|
import com.tangem.datasource.api.common.AuthProvider
|
||||||
import com.tangem.datasource.api.common.response.ApiResponse
|
import com.tangem.datasource.api.common.response.ApiResponse
|
||||||
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException
|
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException
|
||||||
|
|
@ -11,6 +12,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||||
import com.tangem.datasource.api.tangemTech.models.PromocodeActivationBody
|
import com.tangem.datasource.api.tangemTech.models.PromocodeActivationBody
|
||||||
import com.tangem.datasource.api.tangemTech.models.PromocodeActivationResponse
|
import com.tangem.datasource.api.tangemTech.models.PromocodeActivationResponse
|
||||||
import com.tangem.datasource.api.tangemTech.models.WalletResponse
|
import com.tangem.datasource.api.tangemTech.models.WalletResponse
|
||||||
|
import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore
|
||||||
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.domain.account.featuretoggle.AccountsFeatureToggles
|
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||||
|
|
@ -38,12 +40,16 @@ class DefaultWalletsRepositoryTest {
|
||||||
)
|
)
|
||||||
private lateinit var tangemTechApi: TangemTechApi
|
private lateinit var tangemTechApi: TangemTechApi
|
||||||
private lateinit var dispatchers: CoroutineDispatcherProvider
|
private lateinit var dispatchers: CoroutineDispatcherProvider
|
||||||
|
private lateinit var walletServerBinder: WalletServerBinder
|
||||||
|
private lateinit var appsFlyerConversionStore: AppsFlyerConversionStore
|
||||||
|
|
||||||
private val testWalletId = UserWalletId("1234567890abcdef")
|
private val testWalletId = UserWalletId("1234567890abcdef")
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
tangemTechApi = mockk()
|
tangemTechApi = mockk()
|
||||||
|
walletServerBinder = mockk()
|
||||||
|
appsFlyerConversionStore = mockk()
|
||||||
dispatchers = TestingCoroutineDispatcherProvider()
|
dispatchers = TestingCoroutineDispatcherProvider()
|
||||||
repository = DefaultWalletsRepository(
|
repository = DefaultWalletsRepository(
|
||||||
appPreferencesStore = appPreferenceStore,
|
appPreferencesStore = appPreferenceStore,
|
||||||
|
|
@ -52,6 +58,8 @@ class DefaultWalletsRepositoryTest {
|
||||||
seedPhraseNotificationVisibilityStore = mockk(),
|
seedPhraseNotificationVisibilityStore = mockk(),
|
||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
authProvider = mockk(),
|
authProvider = mockk(),
|
||||||
|
walletServerBinder = walletServerBinder,
|
||||||
|
appsFlyerConversionStore = appsFlyerConversionStore,
|
||||||
accountsFeatureToggles = mockk(),
|
accountsFeatureToggles = mockk(),
|
||||||
moshi = mockk(),
|
moshi = mockk(),
|
||||||
)
|
)
|
||||||
|
|
@ -218,10 +226,14 @@ class DefaultWalletsRepositoryTest {
|
||||||
seedPhraseNotificationVisibilityStore = mockk(),
|
seedPhraseNotificationVisibilityStore = mockk(),
|
||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
authProvider = authProvider,
|
authProvider = authProvider,
|
||||||
|
walletServerBinder = walletServerBinder,
|
||||||
|
appsFlyerConversionStore = appsFlyerConversionStore,
|
||||||
accountsFeatureToggles = accountsFeatureToggles,
|
accountsFeatureToggles = accountsFeatureToggles,
|
||||||
moshi = mockk(),
|
moshi = mockk(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
coEvery { appsFlyerConversionStore.get() } returns null
|
||||||
|
|
||||||
coEvery {
|
coEvery {
|
||||||
tangemTechApi.associateApplicationIdWithWallets(
|
tangemTechApi.associateApplicationIdWithWallets(
|
||||||
eq(applicationId),
|
eq(applicationId),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
package com.tangem.domain.wallets.models
|
||||||
|
|
||||||
|
data class AppsFlyerConversionData(val refcode: String, val campaign: String?)
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.tangem.domain.wallets.repository
|
||||||
|
|
||||||
|
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||||
|
|
||||||
|
interface WalletsPromoRepository {
|
||||||
|
|
||||||
|
suspend fun bindRefcodeWithWallets(conversionData: AppsFlyerConversionData)
|
||||||
|
|
||||||
|
suspend fun retryBindRefcodeWithWallets()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.tangem.domain.wallets.usecase
|
||||||
|
|
||||||
|
import arrow.core.Either
|
||||||
|
import arrow.core.raise.catch
|
||||||
|
import arrow.core.raise.either
|
||||||
|
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||||
|
import com.tangem.domain.wallets.repository.WalletsPromoRepository
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class BindRefcodeWithWalletUseCase @Inject constructor(
|
||||||
|
private val walletsPromoRepository: WalletsPromoRepository,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
suspend operator fun invoke(conversionData: AppsFlyerConversionData): Either<Throwable, Unit> = either {
|
||||||
|
mutex.withLock {
|
||||||
|
catch(
|
||||||
|
block = { walletsPromoRepository.bindRefcodeWithWallets(conversionData) },
|
||||||
|
catch = ::raise,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun retry(): Either<Throwable, Unit> = either {
|
||||||
|
mutex.withLock {
|
||||||
|
catch(
|
||||||
|
block = { walletsPromoRepository.retryBindRefcodeWithWallets() },
|
||||||
|
catch = ::raise,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -103,6 +103,7 @@ internal class WalletModel @Inject constructor(
|
||||||
private val singleAccountListSupplier: SingleAccountListSupplier,
|
private val singleAccountListSupplier: SingleAccountListSupplier,
|
||||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||||
private val feedFeatureToggle: FeedFeatureToggle,
|
private val feedFeatureToggle: FeedFeatureToggle,
|
||||||
|
private val bindRefcodeWithWalletUseCase: BindRefcodeWithWalletUseCase,
|
||||||
val screenLifecycleProvider: ScreenLifecycleProvider,
|
val screenLifecycleProvider: ScreenLifecycleProvider,
|
||||||
val innerWalletRouter: InnerWalletRouter,
|
val innerWalletRouter: InnerWalletRouter,
|
||||||
) : Model() {
|
) : Model() {
|
||||||
|
|
@ -137,6 +138,11 @@ internal class WalletModel @Inject constructor(
|
||||||
enableNotificationsIfNeeded()
|
enableNotificationsIfNeeded()
|
||||||
|
|
||||||
clickIntents.initialize(innerWalletRouter, modelScope)
|
clickIntents.initialize(innerWalletRouter, modelScope)
|
||||||
|
|
||||||
|
modelScope.launch {
|
||||||
|
bindRefcodeWithWalletUseCase.retry()
|
||||||
|
.onLeft { Timber.e("Failed to bind refcode with wallets: $it") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onResume() {
|
fun onResume() {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.tangem.feature.wallet.deeplink
|
package com.tangem.feature.wallet.deeplink
|
||||||
|
|
||||||
import com.tangem.blockchain.common.Blockchain
|
import com.tangem.blockchain.common.Blockchain
|
||||||
|
import com.tangem.common.routing.deeplink.DeeplinkConst
|
||||||
import com.tangem.common.routing.deeplink.DeeplinkConst.PROMO_CODE_KEY
|
import com.tangem.common.routing.deeplink.DeeplinkConst.PROMO_CODE_KEY
|
||||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||||
|
|
@ -9,6 +10,7 @@ import com.tangem.core.ui.extensions.mask
|
||||||
import com.tangem.core.ui.extensions.resourceReference
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
import com.tangem.core.ui.message.DialogMessage
|
import com.tangem.core.ui.message.DialogMessage
|
||||||
import com.tangem.core.ui.message.GlobalLoadingMessage
|
import com.tangem.core.ui.message.GlobalLoadingMessage
|
||||||
|
import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore
|
||||||
import com.tangem.domain.models.network.NetworkStatus
|
import com.tangem.domain.models.network.NetworkStatus
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.domain.networks.multi.MultiNetworkStatusProducer
|
import com.tangem.domain.networks.multi.MultiNetworkStatusProducer
|
||||||
|
|
@ -17,8 +19,10 @@ import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
|
||||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
|
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
|
||||||
import com.tangem.domain.wallets.PromoCodeActivationResult
|
import com.tangem.domain.wallets.PromoCodeActivationResult
|
||||||
import com.tangem.domain.wallets.PromoCodeActivationResult.*
|
import com.tangem.domain.wallets.PromoCodeActivationResult.*
|
||||||
|
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||||
import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError
|
import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError
|
||||||
import com.tangem.domain.wallets.usecase.ActivateBitcoinPromocodeUseCase
|
import com.tangem.domain.wallets.usecase.ActivateBitcoinPromocodeUseCase
|
||||||
|
import com.tangem.domain.wallets.usecase.BindRefcodeWithWalletUseCase
|
||||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||||
import com.tangem.feature.wallet.deeplink.analytics.PromoActivationAnalytics
|
import com.tangem.feature.wallet.deeplink.analytics.PromoActivationAnalytics
|
||||||
import com.tangem.feature.wallet.impl.R
|
import com.tangem.feature.wallet.impl.R
|
||||||
|
|
@ -44,10 +48,15 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor(
|
||||||
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||||
private val activateBitcoinPromocodeUseCase: ActivateBitcoinPromocodeUseCase,
|
private val activateBitcoinPromocodeUseCase: ActivateBitcoinPromocodeUseCase,
|
||||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||||
|
private val appsFlyerConversionStore: AppsFlyerConversionStore,
|
||||||
|
private val bindRefcodeWithWalletUseCase: BindRefcodeWithWalletUseCase,
|
||||||
private val analyticsEventsHandler: AnalyticsEventHandler,
|
private val analyticsEventsHandler: AnalyticsEventHandler,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
) : PromoDeeplinkHandler {
|
) : PromoDeeplinkHandler {
|
||||||
|
|
||||||
|
private val refcode: String? = queryParams[DeeplinkConst.REF_KEY]
|
||||||
|
private val campaign: String? = queryParams[DeeplinkConst.CAMPAIGN_KEY]
|
||||||
|
|
||||||
init {
|
init {
|
||||||
analyticsEventsHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart())
|
analyticsEventsHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart())
|
||||||
val promoCode = queryParams[PROMO_CODE_KEY].orEmpty()
|
val promoCode = queryParams[PROMO_CODE_KEY].orEmpty()
|
||||||
|
|
@ -56,6 +65,8 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor(
|
||||||
} else {
|
} else {
|
||||||
findSelectedWallet(promoCode)
|
findSelectedWallet(promoCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveAndBindRefcode()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findSelectedWallet(promoCode: String) {
|
private fun findSelectedWallet(promoCode: String) {
|
||||||
|
|
@ -86,13 +97,9 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor(
|
||||||
|
|
||||||
Timber.tag(LOG_TAG).d("All user network statuses ${networkStatuses?.size}")
|
Timber.tag(LOG_TAG).d("All user network statuses ${networkStatuses?.size}")
|
||||||
|
|
||||||
val cryptoCurrencies = multiWalletCryptoCurrenciesSupplier
|
val cryptoCurrencies = multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
|
||||||
.getSyncOrNull(
|
MultiWalletCryptoCurrenciesProducer.Params(userWallet.walletId),
|
||||||
MultiWalletCryptoCurrenciesProducer.Params(
|
)
|
||||||
userWallet
|
|
||||||
.walletId,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
Timber.tag(LOG_TAG).d("All user cryptoCurrencies on main ${cryptoCurrencies?.size}")
|
Timber.tag(LOG_TAG).d("All user cryptoCurrencies on main ${cryptoCurrencies?.size}")
|
||||||
|
|
||||||
|
|
@ -100,8 +107,7 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor(
|
||||||
Timber.tag(LOG_TAG).d("BitcoinCurrency $bitcoinCurrency")
|
Timber.tag(LOG_TAG).d("BitcoinCurrency $bitcoinCurrency")
|
||||||
|
|
||||||
val bitcoinStatus = networkStatuses?.firstOrNull { status ->
|
val bitcoinStatus = networkStatuses?.firstOrNull { status ->
|
||||||
status.network.id == bitcoinCurrency
|
status.network.id == bitcoinCurrency?.network?.id
|
||||||
?.network?.id
|
|
||||||
}
|
}
|
||||||
Timber.tag(LOG_TAG).d("BitcoinStatus $bitcoinStatus")
|
Timber.tag(LOG_TAG).d("BitcoinStatus $bitcoinStatus")
|
||||||
|
|
||||||
|
|
@ -122,6 +128,7 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor(
|
||||||
Timber.tag(LOG_TAG).d(
|
Timber.tag(LOG_TAG).d(
|
||||||
"Start activation promoCode ${promoCode.mask()} address ${bitcoinAddress.mask()}",
|
"Start activation promoCode ${promoCode.mask()} address ${bitcoinAddress.mask()}",
|
||||||
)
|
)
|
||||||
|
|
||||||
activatePromoCode(bitcoinAddress = bitcoinAddress, promoCode = promoCode)
|
activatePromoCode(bitcoinAddress = bitcoinAddress, promoCode = promoCode)
|
||||||
} else {
|
} else {
|
||||||
uiMessageSender.send(GlobalLoadingMessage(false))
|
uiMessageSender.send(GlobalLoadingMessage(false))
|
||||||
|
|
@ -182,6 +189,19 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun saveAndBindRefcode() {
|
||||||
|
scope.launch(dispatchers.default) {
|
||||||
|
if (!refcode.isNullOrBlank()) {
|
||||||
|
val conversionData = AppsFlyerConversionData(refcode = refcode, campaign = campaign)
|
||||||
|
|
||||||
|
appsFlyerConversionStore.storeIfAbsent(value = conversionData)
|
||||||
|
|
||||||
|
bindRefcodeWithWalletUseCase(conversionData)
|
||||||
|
.onLeft { Timber.e("Failed to bind refcode with wallets: $it") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory : PromoDeeplinkHandler.Factory {
|
interface Factory : PromoDeeplinkHandler.Factory {
|
||||||
override fun create(
|
override fun create(
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import com.tangem.core.decompose.ui.UiMessageSender
|
||||||
import com.tangem.core.ui.extensions.TextReference
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
import com.tangem.core.ui.extensions.resourceReference
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
import com.tangem.core.ui.message.DialogMessage
|
import com.tangem.core.ui.message.DialogMessage
|
||||||
|
import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore
|
||||||
import com.tangem.domain.models.StatusSource
|
import com.tangem.domain.models.StatusSource
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.network.Network
|
import com.tangem.domain.models.network.Network
|
||||||
|
|
@ -25,6 +26,7 @@ import com.tangem.domain.wallets.PromoCodeActivationResult
|
||||||
import com.tangem.domain.wallets.models.GetUserWalletError
|
import com.tangem.domain.wallets.models.GetUserWalletError
|
||||||
import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError
|
import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError
|
||||||
import com.tangem.domain.wallets.usecase.ActivateBitcoinPromocodeUseCase
|
import com.tangem.domain.wallets.usecase.ActivateBitcoinPromocodeUseCase
|
||||||
|
import com.tangem.domain.wallets.usecase.BindRefcodeWithWalletUseCase
|
||||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||||
import com.tangem.feature.wallet.deeplink.DefaultPromoDeeplinkHandler
|
import com.tangem.feature.wallet.deeplink.DefaultPromoDeeplinkHandler
|
||||||
import com.tangem.feature.wallet.deeplink.analytics.PromoActivationAnalytics
|
import com.tangem.feature.wallet.deeplink.analytics.PromoActivationAnalytics
|
||||||
|
|
@ -66,6 +68,12 @@ class DefaultPromoDeeplinkHandlerTest {
|
||||||
@MockK
|
@MockK
|
||||||
private lateinit var analyticsEventHandler: AnalyticsEventHandler
|
private lateinit var analyticsEventHandler: AnalyticsEventHandler
|
||||||
|
|
||||||
|
@MockK
|
||||||
|
private lateinit var bindRefcodeWithWalletUseCase: BindRefcodeWithWalletUseCase
|
||||||
|
|
||||||
|
@MockK
|
||||||
|
private lateinit var appsFlyerConversionStore: AppsFlyerConversionStore
|
||||||
|
|
||||||
private lateinit var messages: MutableList<UiMessage>
|
private lateinit var messages: MutableList<UiMessage>
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
|
|
@ -105,6 +113,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
||||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||||
|
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||||
|
appsFlyerConversionStore = appsFlyerConversionStore,
|
||||||
analyticsEventsHandler = analyticsEventHandler,
|
analyticsEventsHandler = analyticsEventHandler,
|
||||||
dispatchers = dispatcherProvider,
|
dispatchers = dispatcherProvider,
|
||||||
)
|
)
|
||||||
|
|
@ -137,6 +147,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
||||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||||
|
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||||
|
appsFlyerConversionStore = appsFlyerConversionStore,
|
||||||
analyticsEventsHandler = analyticsEventHandler,
|
analyticsEventsHandler = analyticsEventHandler,
|
||||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||||
)
|
)
|
||||||
|
|
@ -168,6 +180,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
||||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||||
|
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||||
|
appsFlyerConversionStore = appsFlyerConversionStore,
|
||||||
analyticsEventsHandler = analyticsEventHandler,
|
analyticsEventsHandler = analyticsEventHandler,
|
||||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||||
)
|
)
|
||||||
|
|
@ -205,6 +219,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
||||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||||
|
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||||
|
appsFlyerConversionStore = appsFlyerConversionStore,
|
||||||
analyticsEventsHandler = analyticsEventHandler,
|
analyticsEventsHandler = analyticsEventHandler,
|
||||||
dispatchers = dispatcherProvider,
|
dispatchers = dispatcherProvider,
|
||||||
)
|
)
|
||||||
|
|
@ -246,6 +262,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
||||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||||
|
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||||
|
appsFlyerConversionStore = appsFlyerConversionStore,
|
||||||
analyticsEventsHandler = analyticsEventHandler,
|
analyticsEventsHandler = analyticsEventHandler,
|
||||||
dispatchers = dispatcherProvider,
|
dispatchers = dispatcherProvider,
|
||||||
)
|
)
|
||||||
|
|
@ -362,6 +380,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
||||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||||
|
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||||
|
appsFlyerConversionStore = appsFlyerConversionStore,
|
||||||
analyticsEventsHandler = analyticsEventHandler,
|
analyticsEventsHandler = analyticsEventHandler,
|
||||||
dispatchers = dispatcherProvider,
|
dispatchers = dispatcherProvider,
|
||||||
)
|
)
|
||||||
|
|
@ -393,6 +413,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
||||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||||
|
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||||
|
appsFlyerConversionStore = appsFlyerConversionStore,
|
||||||
analyticsEventsHandler = analyticsEventHandler,
|
analyticsEventsHandler = analyticsEventHandler,
|
||||||
dispatchers = dispatcherProvider,
|
dispatchers = dispatcherProvider,
|
||||||
)
|
)
|
||||||
|
|
@ -433,6 +455,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
||||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||||
|
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||||
|
appsFlyerConversionStore = appsFlyerConversionStore,
|
||||||
analyticsEventsHandler = analyticsEventHandler,
|
analyticsEventsHandler = analyticsEventHandler,
|
||||||
dispatchers = dispatcherProvider,
|
dispatchers = dispatcherProvider,
|
||||||
)
|
)
|
||||||
|
|
@ -494,6 +518,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
||||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||||
|
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||||
|
appsFlyerConversionStore = appsFlyerConversionStore,
|
||||||
analyticsEventsHandler = analyticsEventHandler,
|
analyticsEventsHandler = analyticsEventHandler,
|
||||||
dispatchers = dispatcherProvider,
|
dispatchers = dispatcherProvider,
|
||||||
)
|
)
|
||||||
|
|
@ -546,6 +572,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
||||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||||
|
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||||
|
appsFlyerConversionStore = appsFlyerConversionStore,
|
||||||
analyticsEventsHandler = analyticsEventHandler,
|
analyticsEventsHandler = analyticsEventHandler,
|
||||||
dispatchers = dispatcherProvider,
|
dispatchers = dispatcherProvider,
|
||||||
)
|
)
|
||||||
|
|
@ -604,6 +632,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
||||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||||
|
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||||
|
appsFlyerConversionStore = appsFlyerConversionStore,
|
||||||
analyticsEventsHandler = analyticsEventHandler,
|
analyticsEventsHandler = analyticsEventHandler,
|
||||||
dispatchers = dispatcherProvider,
|
dispatchers = dispatcherProvider,
|
||||||
)
|
)
|
||||||
|
|
@ -661,6 +691,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
||||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||||
|
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||||
|
appsFlyerConversionStore = appsFlyerConversionStore,
|
||||||
analyticsEventsHandler = analyticsEventHandler,
|
analyticsEventsHandler = analyticsEventHandler,
|
||||||
dispatchers = dispatcherProvider,
|
dispatchers = dispatcherProvider,
|
||||||
)
|
)
|
||||||
|
|
@ -714,6 +746,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
||||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||||
|
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||||
|
appsFlyerConversionStore = appsFlyerConversionStore,
|
||||||
analyticsEventsHandler = analyticsEventHandler,
|
analyticsEventsHandler = analyticsEventHandler,
|
||||||
dispatchers = dispatcherProvider,
|
dispatchers = dispatcherProvider,
|
||||||
)
|
)
|
||||||
|
|
@ -767,6 +801,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
||||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||||
|
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||||
|
appsFlyerConversionStore = appsFlyerConversionStore,
|
||||||
analyticsEventsHandler = analyticsEventHandler,
|
analyticsEventsHandler = analyticsEventHandler,
|
||||||
dispatchers = dispatcherProvider,
|
dispatchers = dispatcherProvider,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
androidGradlePlugin = "8.10.1"
|
androidGradlePlugin = "8.10.1"
|
||||||
firebaseCrashlytics = "3.0.1"
|
firebaseCrashlytics = "3.0.1"
|
||||||
googleServices = "4.4.1"
|
googleServices = "4.4.1"
|
||||||
|
playServices = "18.10.0"
|
||||||
|
playServicesAds = "18.2.0"
|
||||||
kotlin = "2.1.10"
|
kotlin = "2.1.10"
|
||||||
ksp = "2.1.10-1.0.30"
|
ksp = "2.1.10-1.0.30"
|
||||||
firebasePerf = "1.4.2"
|
firebasePerf = "1.4.2"
|
||||||
|
|
@ -255,6 +257,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 = { module = "com.google.android.play:review", version.ref = "googlePlayReview" }
|
||||||
googlePlay-review-ktx = { module = "com.google.android.play:review-ktx", version.ref = "googlePlayReviewKtx" }
|
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-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-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
|
||||||
hilt-core = { module = "com.google.dagger:hilt-core", 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" }
|
hilt-kapt = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue