Updated on 2026-08-14
This commit is contained in:
commit
6201e61e2a
73 changed files with 2868 additions and 436 deletions
|
|
@ -108,6 +108,9 @@ configurations.androidTestImplementation {
|
|||
exclude(module = "protobuf-lite")
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.domain.legacy)
|
||||
|
|
@ -363,6 +366,8 @@ dependencies {
|
|||
implementation(deps.googlePlay.review)
|
||||
implementation(deps.googlePlay.review.ktx)
|
||||
implementation(deps.googlePlay.services.wallet)
|
||||
implementation(deps.googlePlay.services)
|
||||
implementation(deps.googlePlay.advertising)
|
||||
coreLibraryDesugaring(deps.desugar)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.reKotlin)
|
||||
|
|
@ -393,11 +398,10 @@ dependencies {
|
|||
implementation(files("libs/dexprotector-annotations.jar"))
|
||||
|
||||
/** Testing libraries */
|
||||
testImplementation(projects.test.core)
|
||||
testImplementation(projects.common.test)
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
androidTestImplementation(deps.test.junit.android)
|
||||
androidTestImplementation(deps.test.espresso)
|
||||
androidTestImplementation(deps.test.espresso.intents)
|
||||
|
|
|
|||
|
|
@ -11,9 +11,15 @@
|
|||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<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 android:name="android.permission.ACCESS_COARSE_LOCATION" tools:node="remove" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" tools:node="remove" />
|
||||
<uses-permission
|
||||
android:name="android.permission.ACCESS_FINE_LOCATION"
|
||||
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.autofocus" />
|
||||
|
|
@ -36,7 +42,6 @@
|
|||
|
||||
<application
|
||||
android:name="com.tangem.tap.TangemHiltApplication"
|
||||
android:theme="@style/AppTheme"
|
||||
android:allowBackup="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
|
|
@ -49,6 +54,7 @@
|
|||
android:resizeableActivity="@bool/resizeable_activity"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:ignore="GoogleAppIndexingWarning"
|
||||
tools:replace="android:allowBackup, android:dataExtractionRules, android:fullBackupContent, android:label, android:largeHeap">
|
||||
|
||||
|
|
@ -286,9 +292,20 @@
|
|||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:scheme="https"
|
||||
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>
|
||||
</activity>
|
||||
|
||||
|
|
@ -326,6 +343,6 @@
|
|||
</application>
|
||||
<queries>
|
||||
<!-- Needed from Android 11 to open Google Wallet for payment with Visa card -->
|
||||
<package android:name="com.google.android.apps.walletnfcrel" />
|
||||
</queries>
|
||||
<package android:name="com.google.android.apps.walletnfcrel" />
|
||||
</queries>
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
|||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
|
||||
import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerClient
|
||||
import com.tangem.tap.common.log.TangemAppLoggerInitializer
|
||||
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
|
|
@ -157,4 +158,6 @@ interface ApplicationEntryPoint {
|
|||
fun getTrackingContextProxy(): TrackingContextProxy
|
||||
|
||||
fun getABTestsManager(): ABTestsManager
|
||||
|
||||
fun getAppsFlyerClientFactory(): AppsFlyerClient.Factory
|
||||
}
|
||||
|
|
@ -66,6 +66,7 @@ import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
|
|||
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
|
||||
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
|
||||
import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerAnalyticsHandler
|
||||
import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerClient
|
||||
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
|
||||
import com.tangem.tap.common.images.createCoilImageLoader
|
||||
import com.tangem.tap.common.log.TangemAppLoggerInitializer
|
||||
|
|
@ -248,6 +249,9 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
|||
private val abTestsManager: ABTestsManager
|
||||
get() = entryPoint.getABTestsManager()
|
||||
|
||||
private val appsFlyerClientFactory: AppsFlyerClient.Factory
|
||||
get() = entryPoint.getAppsFlyerClientFactory()
|
||||
|
||||
// endregion
|
||||
|
||||
private val appScope = MainScope()
|
||||
|
|
@ -421,7 +425,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
|||
val factory = AnalyticsFactory()
|
||||
factory.addHandlerBuilder(AmplitudeAnalyticsHandler.Builder())
|
||||
factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder())
|
||||
factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder())
|
||||
factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder(appsFlyerClientFactory))
|
||||
|
||||
factory.addFilter(oneTimeEventFilter)
|
||||
factory.addFilter(AppsFlyerEventFilter())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,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.AppsFlyerStore
|
||||
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 appsFlyerStore: AppsFlyerStore,
|
||||
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 {
|
||||
appsFlyerStore.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,65 +1,54 @@
|
|||
package com.tangem.tap.common.analytics.handlers.appsflyer
|
||||
|
||||
import android.content.Context
|
||||
import com.appsflyer.AppsFlyerConversionListener
|
||||
import com.appsflyer.AppsFlyerLib
|
||||
import com.appsflyer.attribution.AppsFlyerRequestListener
|
||||
import com.tangem.core.analytics.api.EventLogger
|
||||
import com.tangem.core.analytics.api.UserIdHolder
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
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.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
interface AppsFlyerAnalyticsClient : EventLogger, UserIdHolder
|
||||
|
||||
internal class AppsFlyerClient(
|
||||
private val context: Context,
|
||||
key: String,
|
||||
class AppsFlyerClient @AssistedInject constructor(
|
||||
@Assisted apiKey: String,
|
||||
@ApplicationContext private val context: Context,
|
||||
appsFlyerDeepLinkListener: AppsFlyerDeepLinkListener,
|
||||
tangemAFConversionListener: TangemAFConversionListener,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
) : AppsFlyerAnalyticsClient {
|
||||
|
||||
private val appsFlyerLib: AppsFlyerLib = AppsFlyerLib.getInstance()
|
||||
private val eventConverter = UnderscoreAnalyticsEventConverter()
|
||||
private val logEventListener = object : AppsFlyerRequestListener {
|
||||
override fun onSuccess() {
|
||||
Timber.tag("AppsFlyerClient").i("AppsFlyerRequestListener send")
|
||||
}
|
||||
|
||||
override fun onError(p0: Int, p1: String) {
|
||||
Timber.tag("AppsFlyerClient").e("AppsFlyerRequestListener onError: $p0, $p1")
|
||||
}
|
||||
}
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
||||
init {
|
||||
val conversionListener = object : AppsFlyerConversionListener {
|
||||
override fun onConversionDataSuccess(p0: Map<String?, Any?>?) {
|
||||
Timber.i("AppsFlyer conversion data success: ${p0.orEmpty()}")
|
||||
}
|
||||
override fun onConversionDataFail(p0: String?) {
|
||||
Timber.e("AppsFlyer conversion data failure: ${p0.orEmpty()}")
|
||||
}
|
||||
override fun onAppOpenAttribution(p0: Map<String?, String?>?) {
|
||||
Timber.i("AppsFlyer app open attribution: ${p0.orEmpty()}")
|
||||
}
|
||||
override fun onAttributionFailure(p0: String?) {
|
||||
Timber.e("AppsFlyer attribution failure: ${p0.orEmpty()}")
|
||||
}
|
||||
}
|
||||
|
||||
appsFlyerLib.run {
|
||||
with(appsFlyerLib) {
|
||||
setAppId(context.packageName)
|
||||
setDebugLog(true)
|
||||
|
||||
init(key, conversionListener, context)
|
||||
Timber.i("Starting AppsFlyer SDK")
|
||||
start(context, key, object : AppsFlyerRequestListener {
|
||||
override fun onSuccess() {
|
||||
Timber.i("AppsFlyer initialized successfully")
|
||||
}
|
||||
subscribeForDeepLink(appsFlyerDeepLinkListener)
|
||||
|
||||
override fun onError(p0: Int, p1: String) {
|
||||
Timber.e("AppsFlyer initialization error: $p0, $p1")
|
||||
}
|
||||
})
|
||||
init(apiKey, tangemAFConversionListener, context)
|
||||
|
||||
Timber.i("Starting AppsFlyer SDK")
|
||||
start(context, apiKey, InitializationListener)
|
||||
Timber.i("AppsFlyer SDK started")
|
||||
|
||||
saveUID()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -77,7 +66,42 @@ internal class AppsFlyerClient(
|
|||
context,
|
||||
event,
|
||||
eventConverter.convertEventParams(params),
|
||||
logEventListener,
|
||||
LogEventListener,
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveUID() {
|
||||
coroutineScope.launch {
|
||||
val appsFlyerUID = appsFlyerLib.getAppsFlyerUID(context)
|
||||
|
||||
if (appsFlyerUID != null) {
|
||||
appsFlyerStore.storeUIDIfAbsent(appsFlyerUID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object InitializationListener : AppsFlyerRequestListener {
|
||||
override fun onSuccess() {
|
||||
Timber.d("AppsFlyer initialized successfully")
|
||||
}
|
||||
|
||||
override fun onError(p0: Int, p1: String) {
|
||||
Timber.e("AppsFlyer initialization error: $p0, $p1")
|
||||
}
|
||||
}
|
||||
|
||||
private object LogEventListener : AppsFlyerRequestListener {
|
||||
override fun onSuccess() {
|
||||
Timber.tag("AppsFlyerClient").i("AppsFlyerRequestListener send")
|
||||
}
|
||||
|
||||
override fun onError(p0: Int, p1: String) {
|
||||
Timber.tag("AppsFlyerClient").e("AppsFlyerRequestListener onError: $p0, $p1")
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(apiKey: String): AppsFlyerClient
|
||||
}
|
||||
}
|
||||
|
|
@ -47,7 +47,10 @@ class AppsFlyerAnalyticsHandler(
|
|||
const val ID = "AppsFlyer"
|
||||
}
|
||||
|
||||
class Builder : AnalyticsHandlerBuilder {
|
||||
internal class Builder(
|
||||
private val appsFlyerClientFactory: AppsFlyerClient.Factory,
|
||||
) : AnalyticsHandlerBuilder {
|
||||
|
||||
init {
|
||||
Timber.tag("AppsFlyer").i("AppsFlyer Analytics Handler Builder created")
|
||||
}
|
||||
|
|
@ -58,7 +61,7 @@ class AppsFlyerAnalyticsHandler(
|
|||
AppsFlyerLogClient(data.jsonConverter)
|
||||
} else {
|
||||
Timber.tag("AppsFlyer").i("AppsFlyer log disabled, real client created")
|
||||
AppsFlyerClient(data.application, data.config.appsFlyerApiKey)
|
||||
appsFlyerClientFactory.create(apiKey = data.config.appsFlyerApiKey)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.AppsFlyerStore
|
||||
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 appsFlyerStore: AppsFlyerStore = mockk(relaxUnitFun = true)
|
||||
private val handler = AppsFlyerReferralParamsHandler(
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
clearMocks(appsFlyerStore)
|
||||
}
|
||||
|
||||
@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 { appsFlyerStore.storeIfAbsent(value = value) }
|
||||
} else {
|
||||
coVerify(inverse = true) { appsFlyerStore.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 { appsFlyerStore.storeIfAbsent(value = value) }
|
||||
} else {
|
||||
coVerify(inverse = true) { appsFlyerStore.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 TRANSACTION_ID_KEY = "transaction_id"
|
||||
const val PROMO_CODE_KEY = "promo_code"
|
||||
const val REF_KEY = "ref"
|
||||
const val CAMPAIGN_KEY = "campaign"
|
||||
const val NAME_KEY = "name"
|
||||
}
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
},
|
||||
{
|
||||
"name": "HOT_WALLET_VISIBLE",
|
||||
"version": "undefined"
|
||||
"version": "5.32.1"
|
||||
},
|
||||
{
|
||||
"name": "TANGEM_PAY_ENABLED",
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ interface TangemTechApi {
|
|||
@Body userTokens: UserTokensResponse,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
// region Referral
|
||||
/** Returns referral status by [walletId] */
|
||||
@GET("v1/referral/{walletId}")
|
||||
suspend fun getReferralStatus(@Path("walletId") walletId: String): ApiResponse<ReferralResponse>
|
||||
|
|
@ -64,6 +65,10 @@ interface TangemTechApi {
|
|||
@POST("v1/referral")
|
||||
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")
|
||||
suspend fun getQuotes(
|
||||
@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.WalletType
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||
|
||||
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(
|
||||
walletId = userWallet.walletId.stringValue,
|
||||
name = userWallet.name,
|
||||
walletType = WalletType.from(userWallet),
|
||||
refcode = conversionData?.refcode,
|
||||
campaign = conversionData?.campaign,
|
||||
cards = publicKeys?.map { publicKeyById ->
|
||||
CardInfoBody(
|
||||
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,
|
||||
)
|
||||
|
|
@ -12,4 +12,5 @@ data class NotificationApplicationCreateBody(
|
|||
@Json(name = "version") val version: String? = null,
|
||||
@Json(name = "language") val language: String? = null,
|
||||
@Json(name = "timezone") val timezone: String? = null,
|
||||
@Json(name = "appsflyerId") val appsflyerId: String? = null,
|
||||
)
|
||||
|
|
@ -17,6 +17,8 @@ data class UserTokensResponse(
|
|||
@Json(name = "notifyStatus") val notifyStatus: Boolean? = null,
|
||||
@Json(name = "name") val walletName: String? = 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)
|
||||
|
|
|
|||
|
|
@ -9,4 +9,6 @@ data class WalletIdBody(
|
|||
@Json(name = "name") val name: String,
|
||||
@Json(name = "type") val walletType: WalletType? = 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.AppsFlyerStore
|
||||
import com.tangem.datasource.local.appsflyer.DefaultAppsFlyerStore
|
||||
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 provideAppFlyerStore(appPreferencesStore: AppPreferencesStore): AppsFlyerStore {
|
||||
return DefaultAppsFlyerStore(appPreferencesStore = appPreferencesStore)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.datasource.local.appsflyer
|
||||
|
||||
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||
|
||||
interface AppsFlyerStore {
|
||||
|
||||
suspend fun get(): AppsFlyerConversionData?
|
||||
|
||||
suspend fun getUID(): String?
|
||||
|
||||
suspend fun store(value: AppsFlyerConversionData)
|
||||
|
||||
suspend fun storeIfAbsent(value: AppsFlyerConversionData)
|
||||
|
||||
suspend fun storeUIDIfAbsent(value: String)
|
||||
}
|
||||
|
|
@ -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,70 @@
|
|||
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.getSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.storeObject
|
||||
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultAppsFlyerStore(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : AppsFlyerStore {
|
||||
|
||||
override suspend fun get(): AppsFlyerConversionData? {
|
||||
val dto = appPreferencesStore.getObjectSyncOrNull<ConversionDataDTO>(CONVERSION_DATA_KEY) ?: return null
|
||||
|
||||
return ConversionDataConverter.convertBack(value = dto).also {
|
||||
Timber.i("Getting conversion data from store: $it")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getUID(): String? = appPreferencesStore.getSyncOrNull(UID_KEY)
|
||||
|
||||
override suspend fun store(value: AppsFlyerConversionData) {
|
||||
Timber.i("Storing conversion data to store: $value")
|
||||
|
||||
val dto = ConversionDataConverter.convert(value)
|
||||
|
||||
appPreferencesStore.storeObject(CONVERSION_DATA_KEY, dto)
|
||||
}
|
||||
|
||||
override suspend fun storeIfAbsent(value: AppsFlyerConversionData) {
|
||||
Timber.i("Storing conversion data to store if absent: $value")
|
||||
appPreferencesStore.editData { preferences ->
|
||||
val saved = preferences[CONVERSION_DATA_KEY]
|
||||
|
||||
if (saved == null) {
|
||||
Timber.i("Conversion data is absent, storing $value")
|
||||
preferences.setObject(CONVERSION_DATA_KEY, ConversionDataConverter.convert(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun storeUIDIfAbsent(value: String) {
|
||||
Timber.i("Storing UID to store if absent: $value")
|
||||
appPreferencesStore.editData { preferences ->
|
||||
val saved = preferences[UID_KEY]
|
||||
|
||||
if (saved == null) {
|
||||
Timber.i("UID is absent, storing $value")
|
||||
preferences[UID_KEY] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val UID_KEY = stringPreferencesKey("APPS_FLYER_UID")
|
||||
val CONVERSION_DATA_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
|
||||
val result = WalletIdBodyConverter.convert(userWallet, publicKeys)
|
||||
val result = WalletIdBodyConverter.convert(
|
||||
userWallet = userWallet,
|
||||
conversionData = null,
|
||||
publicKeys = publicKeys,
|
||||
)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(
|
||||
|
|
@ -68,7 +72,11 @@ class WalletIdBodyConverterTest {
|
|||
val publicKeys = emptyMap<String, String>()
|
||||
|
||||
// WHEN
|
||||
val result = WalletIdBodyConverter.convert(userWallet, publicKeys)
|
||||
val result = WalletIdBodyConverter.convert(
|
||||
userWallet = userWallet,
|
||||
conversionData = null,
|
||||
publicKeys = publicKeys,
|
||||
)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(
|
||||
|
|
|
|||
|
|
@ -123,8 +123,8 @@
|
|||
<string name="auth_info_subtitle">Wähle eine Wallet zum Einloggen</string>
|
||||
<string name="auth_info_title">Willkommen zurück!</string>
|
||||
<plurals name="auth_wallet_hardware_description">
|
||||
<item quantity="one">%s Karte</item>
|
||||
<item quantity="other">%s Karten</item>
|
||||
<item quantity="one">%d Karte</item>
|
||||
<item quantity="other">%d Karten</item>
|
||||
</plurals>
|
||||
<string name="auth_wallet_mobile_description">Mobile-Wallet</string>
|
||||
<string name="backup_complete_description">Du hast Deine Wallet erfolgreich gesichert.</string>
|
||||
|
|
@ -313,8 +313,8 @@
|
|||
<string name="common_network_fee_title">Netzgebühr</string>
|
||||
<string name="common_network_fee_warning_content">Der überwiesene Betrag wird um %1$s (%2$s) gekürzt, um die gewählte Gebührenhöhe zu decken.</string>
|
||||
<plurals name="common_networks_count">
|
||||
<item quantity="one">%1$s Netzwerk</item>
|
||||
<item quantity="other">%1$s Netzwerke</item>
|
||||
<item quantity="one">%d Netzwerk</item>
|
||||
<item quantity="other">%d Netzwerke</item>
|
||||
</plurals>
|
||||
<string name="common_new_address">Neue Adresse</string>
|
||||
<string name="common_news">Nachrichten</string>
|
||||
|
|
@ -323,6 +323,7 @@
|
|||
<string name="common_no">Nein</string>
|
||||
<string name="common_no_address">Keine Adresse</string>
|
||||
<string name="common_not_added">Nicht hinzugefügt</string>
|
||||
<string name="common_not_available">Nicht verfügbar</string>
|
||||
<string name="common_not_now">Nicht jetzt</string>
|
||||
<string name="common_now">Jetzt</string>
|
||||
<string name="common_ok">OK</string>
|
||||
|
|
@ -561,6 +562,8 @@
|
|||
<string name="feedback_subject_support_tangem">Feedback zu Tangem</string>
|
||||
<string name="feedback_subject_tx_failed">Eine Transaktion kann nicht gesendet werden</string>
|
||||
<string name="feedback_token_description_error">Fehler in der Coinbeschreibung</string>
|
||||
<string name="gasless_not_enough_funds_to_cover_token_fee">Nicht genügend Mittel</string>
|
||||
<string name="gasless_transaction_fee">Transaktionsgebühr</string>
|
||||
<string name="generic_error">Es ist ein Fehler aufgetreten</string>
|
||||
<string name="generic_error_code">Es ist ein Fehler aufgetreten. Code: %s.</string>
|
||||
<string name="generic_requires_memo_error">Memo erforderlich</string>
|
||||
|
|
@ -602,7 +605,7 @@
|
|||
<string name="hw_backup_hardware_create_title">Neue Wallet erstellen</string>
|
||||
<string name="hw_backup_hardware_description">Erhöhe Deine Sicherheit mit der überlegenen Hardware-Wallet von Tangem.</string>
|
||||
<string name="hw_backup_hardware_title">Hardware-Wallet</string>
|
||||
<string name="hw_backup_hardware_upgrade_description">Übertrage Deine aktuelle Wallet in die Tangem Wallet.</string>
|
||||
<string name="hw_backup_hardware_upgrade_description">Übertrage Deine aktuelle Wallet in die Tangem Hardware-Wallet.</string>
|
||||
<string name="hw_backup_hardware_upgrade_title">Aktualisiere Deine aktuelle Wallet</string>
|
||||
<string name="hw_backup_need_action">Gehe zum Backup</string>
|
||||
<string name="hw_backup_need_description">Bitte sicher Deine Wallet, bevor Du einen Zugangscode erstellst.</string>
|
||||
|
|
@ -827,7 +830,7 @@
|
|||
<string name="news_quick_recap">Kurze Zusammenfassung</string>
|
||||
<string name="news_related_news">Verwandte Nachrichten</string>
|
||||
<string name="news_related_tokens">Verwandte Token</string>
|
||||
<string name="news_sources">Quellen</string>
|
||||
<string name="news_sources">Verwandte Nachrichten</string>
|
||||
<string name="news_stay_in_the_loop">Auf dem Laufenden bleiben</string>
|
||||
<string name="nfc_error_unavailable">NFC ist auf deinem Gerät nicht verfügbar</string>
|
||||
<string name="nft_about_title">Über NFT</string>
|
||||
|
|
@ -995,6 +998,7 @@
|
|||
<string name="onboarding_wallet_info_title_fourth">Zugangscode wiederherstellen</string>
|
||||
<string name="onboarding_wallet_info_title_second">Identische Karten oder Ring</string>
|
||||
<string name="onboarding_wallet_info_title_third">Zugangscode</string>
|
||||
<string name="only_one_mobile_wallet_explanation">Du kannst nur eine mobile Wallet verwenden. Aktualisiere diese auf einer Tangem-Hardware-Wallet oder füge eine neue Hardware-Wallet hinzu.</string>
|
||||
<string name="onramp_all_offers_button_title">Alle Angebote</string>
|
||||
<string name="onramp_avaiable_with_payment_methods">Verfügbar mit %s</string>
|
||||
<string name="onramp_choose_provider_title_hint">Anbieter erleichtern Transaktionen</string>
|
||||
|
|
@ -1043,6 +1047,7 @@
|
|||
<string name="onramp_transaction_status_footer_text">Du kannst diesen Bildschirm schließen und den Transaktionsstatus auf dem Bildschirm mit den Token-Details überprüfen.</string>
|
||||
<string name="onramp_up_to_rate">Bis zu</string>
|
||||
<string name="onramp_via">Über</string>
|
||||
<string name="onramp_via_provider">über %s</string>
|
||||
<string name="onramp_you_will_pay_title">Du zahlst</string>
|
||||
<string name="organize_tokens_group">Gruppe erstellen</string>
|
||||
<string name="organize_tokens_sort_by_balance">Nach Guthaben</string>
|
||||
|
|
@ -1265,6 +1270,7 @@
|
|||
<string name="staking_amount_tron_integer_error">Der Stakingbetrag wird aufgrund der Netzwerkregeln auf %1$s TRX aufgerundet.</string>
|
||||
<string name="staking_amount_tron_integer_error_unstaking">Der Betrag der unstaked wird, wird aufgrund der Netzwerkregeln auf %1$s TRX gerundet.</string>
|
||||
<string name="staking_apr_earn_badge">Effektiver Jahreszins %1$s%%</string>
|
||||
<string name="staking_cardano_details_rewards_info_text">Deine Staking-Belohnungen beginnen nach 5 Epochen (~25 Tage), während Deine Delegation vom Netzwerk registriert und gezählt wird.</string>
|
||||
<string name="staking_claim_unstaked">Nicht gestakte beanspruche</string>
|
||||
<string name="staking_details_account_fee">Gebühr für das Staking-Konto</string>
|
||||
<string name="staking_details_account_fee_info">Ein Staking-Konto ist ein spezielles Konto, auf dem eingesetzte SOL-Token gespeichert werden. Es wird erstellt, indem Sie Ihre Token an einen Validator delegieren, um an der Transaktionsvalidierung teilzunehmen und Belohnungen zu erhalten. Für die Einrichtung des Staking-Kontos wird eine geringe Gebühr erhoben, die nach Abschluss des Stakings zurückerstattet wird.</string>
|
||||
|
|
@ -1275,6 +1281,7 @@
|
|||
<string name="staking_details_apr">Effektiver Jahreszins</string>
|
||||
<string name="staking_details_apy">APY</string>
|
||||
<string name="staking_details_auto_claiming_rewards_daily_text">Belohnungen sammeln sich täglich automatisch in deinem Staking-Konto an.</string>
|
||||
<string name="staking_details_autocompound_rewards_earned">Die Belohnungen werden auf Dein Stakingguthaben aufgezinst. Verdiente Gelder: %s</string>
|
||||
<string name="staking_details_available">Verfügbar</string>
|
||||
<string name="staking_details_average_reward_rate">Durchschnittliche Belohnungsquote</string>
|
||||
<string name="staking_details_banner_text">Wie funktioniert Staking?</string>
|
||||
|
|
@ -1419,6 +1426,7 @@
|
|||
<string name="swap_story_third_subtitle">Problemlos und intuitiv, sodass Deine Token mit nur wenigen Handgriffen getauscht werden können</string>
|
||||
<string name="swap_story_third_title">Einfach bequem</string>
|
||||
<string name="swap_via_provider">Tausch über Anbieter</string>
|
||||
<string name="swap_your_assets_title">Dein Vermögen</string>
|
||||
<string name="swapping_alert_cex_description">Der Betrag umfasst:\n- Gebühr des Dienstanbieters\n- Netzgebühr für die Rücksendung von %s von der Vermittlungsstelle an die Adresse des Nutzers.</string>
|
||||
<string name="swapping_alert_cex_description_with_slippage">Der Betrag beinhaltet:\n• Honorar des Dienstleisters\n• Netzwerkgebühr für das Senden %1$s von der Börse zurück an die Adresse des Benutzers. \n\n Provider-Slippage kann bis zu %2$s</string>
|
||||
<string name="swapping_alert_dex_description">Der Betrag enthält die Gebühren des Dienstleisters.</string>
|
||||
|
|
@ -1433,6 +1441,7 @@
|
|||
<string name="swapping_insufficient_funds">Unzureichende Mittel</string>
|
||||
<string name="swapping_permission_header">Erlaubnis erteilen</string>
|
||||
<string name="swapping_swap_action">Tauschen</string>
|
||||
<string name="swapping_swap_action_in_progress">Tauschen...</string>
|
||||
<string name="swapping_to_title">Du erhältst</string>
|
||||
<string name="swapping_token_list_title">Token auswählen</string>
|
||||
<string name="swapping_token_not_available">Nicht verfügbar</string>
|
||||
|
|
@ -1527,10 +1536,15 @@
|
|||
<string name="tangempay_kyc_confirm_cancellation_description">Möchtest Du den KYC wirklich abbrechen? Du kannst später jederzeit weiter machen.</string>
|
||||
<string name="tangempay_kyc_failed_description">Wir konnten Ihr Profil nicht verifizieren. Bei Fragen wenden Sie sich bitte an den Support.</string>
|
||||
<string name="tangempay_kyc_failed_title">Leider konnten wir Ihre Identität nicht verifizieren</string>
|
||||
<string name="tangempay_kyc_has_failed">KYC ist gescheitert</string>
|
||||
<string name="tangempay_kyc_in_progress">KYC in Bearbeitung</string>
|
||||
<string name="tangempay_kyc_in_progress_notification_button">Status anzeigen</string>
|
||||
<string name="tangempay_kyc_in_progress_notification_title">KYC für Tangem Pay in Arbeit</string>
|
||||
<string name="tangempay_kyc_in_progress_popup_description">Dokumente werden in der Regel innerhalb von 5 Minuten automatisch überprüft. In seltenen Fällen, wenn eine manuelle Überprüfung erforderlich ist, kann es bis zu 48 Stunden dauern.</string>
|
||||
<string name="tangempay_kyc_rejected">KYC abgelehnt</string>
|
||||
<string name="tangempay_kyc_rejected_button_text">KYC-Sperre ausblenden</string>
|
||||
<string name="tangempay_kyc_rejected_description">Entschuldigung, wir konnten dies nicht überprüfen.</string>
|
||||
<string name="tangempay_kyc_rejected_description_span">Dein Profil.</string>
|
||||
<string name="tangempay_onboarding_banner_description">Holen Sie sich Ihre kostenlose virtuelle Tangem Visa-Karte</string>
|
||||
<string name="tangempay_onboarding_banner_title">Nutzen Sie USDC für alltägliche Zahlungen</string>
|
||||
<string name="tangempay_onboarding_get_card_button_text">Karte erhalten</string>
|
||||
|
|
|
|||
|
|
@ -174,8 +174,8 @@
|
|||
<item quantity="other">%d tarjetas</item>
|
||||
</plurals>
|
||||
<plurals name="card_label_token_count">
|
||||
<item quantity="one">%s token</item>
|
||||
<item quantity="other">%s tokens</item>
|
||||
<item quantity="one">%d token</item>
|
||||
<item quantity="other">%d tokens</item>
|
||||
</plurals>
|
||||
<string name="card_reset_alert_continue_message">Por favor restablezca el siguiente dispositivo para continuar.</string>
|
||||
<string name="card_reset_alert_continue_title">Restablecer billetera</string>
|
||||
|
|
@ -1043,6 +1043,7 @@
|
|||
<string name="onramp_transaction_status_footer_text">Puede comprobar el estado de la transacción desde la página detallada del token</string>
|
||||
<string name="onramp_up_to_rate">Hasta</string>
|
||||
<string name="onramp_via">Vía</string>
|
||||
<string name="onramp_via_provider">vía %s</string>
|
||||
<string name="onramp_you_will_pay_title">Pagará</string>
|
||||
<string name="organize_tokens_group">Agrupar</string>
|
||||
<string name="organize_tokens_sort_by_balance">Por saldo</string>
|
||||
|
|
|
|||
|
|
@ -173,8 +173,8 @@
|
|||
<item quantity="other">%d cartes</item>
|
||||
</plurals>
|
||||
<plurals name="card_label_token_count">
|
||||
<item quantity="one">%s token</item>
|
||||
<item quantity="other">%s tokens</item>
|
||||
<item quantity="one">%d token</item>
|
||||
<item quantity="other">%d tokens</item>
|
||||
</plurals>
|
||||
<string name="card_reset_alert_continue_message">Veuillez réinitialiser l\'appareil suivant pour continuer.</string>
|
||||
<string name="card_reset_alert_continue_title">Réinitialisation du portefeuille</string>
|
||||
|
|
@ -1039,6 +1039,7 @@
|
|||
<string name="onramp_transaction_status_footer_text">Vous pouvez vérifier l\'état de la transaction à partir de la page du jeton</string>
|
||||
<string name="onramp_up_to_rate">Jusqu\'à</string>
|
||||
<string name="onramp_via">Via</string>
|
||||
<string name="onramp_via_provider">via %s</string>
|
||||
<string name="onramp_you_will_pay_title">Vous paierez</string>
|
||||
<string name="organize_tokens_group">Grouper</string>
|
||||
<string name="organize_tokens_sort_by_balance">Par solde</string>
|
||||
|
|
|
|||
|
|
@ -130,9 +130,9 @@
|
|||
<string name="backup_complete_seed_description">これらの単語は、紛失すると復元できません。安全に保管してください。</string>
|
||||
<string name="backup_complete_title">バックアップが完了しました</string>
|
||||
<string name="backup_info_description">シークレットリカバリーフレーズは、ウォレットへのアクセスおよび復元に使用する、%s個のランダムな単語のセットです。</string>
|
||||
<string name="backup_info_keep_description">これらの単語は紛失した場合、復元できません。大切に保管してください。</string>
|
||||
<string name="backup_info_keep_description">このフレーズを紛失すると、復元することはできません。厳重に保管してください。</string>
|
||||
<string name="backup_info_keep_title">安全に保管してください</string>
|
||||
<string name="backup_info_save_description">これらの%s個の単語を安全な場所に保存し、決して誰とも共有しないでください。</string>
|
||||
<string name="backup_info_save_description">これらの%sの単語は、あなただけがアクセスできる安全な場所に保管し、決して他人と共有しないでください。</string>
|
||||
<string name="backup_info_save_title">復元は不可能です</string>
|
||||
<string name="backup_info_title">リカバリーフレーズ</string>
|
||||
<string name="backup_seed_caution">以下の%s個の単語が、あなたのウォレットのリカバリーフレーズです。これらの単語は絶対に他人と共有しないでください。Tangemがこれらを尋ねることは決してありません。デバイスを紛失した場合、このフレーズを使ってウォレットを復元できます。</string>
|
||||
|
|
@ -172,7 +172,7 @@
|
|||
<item quantity="other">%d デバイス</item>
|
||||
</plurals>
|
||||
<plurals name="card_label_token_count">
|
||||
<item quantity="other">%1$dトークン</item>
|
||||
<item quantity="other">%dトークン</item>
|
||||
</plurals>
|
||||
<string name="card_reset_alert_continue_message">続行するには次のデバイスをリセットしてください。</string>
|
||||
<string name="card_reset_alert_continue_title">ウォレットのリセット</string>
|
||||
|
|
@ -446,7 +446,7 @@
|
|||
<string name="details_row_title_signed_hashes">署名済み</string>
|
||||
<string name="details_send_feedback">フィードバックを送信</string>
|
||||
<string name="details_title">詳細</string>
|
||||
<string name="details_wallets_section_description">モバイルウォレットは同時に1つのみ利用できます。Tangemコールドウォレットにアップグレードすることも、新しいコールドウォレットと併用することも可能です。</string>
|
||||
<string name="details_wallets_section_description">モバイルウォレットは1つしか作成できません。Tangemハードウェアウォレットにアップグレードするか、新しいハードウェアウォレットを追加してください。</string>
|
||||
<string name="disclaimer_error_loading">インターネット接続を確認するか、別のネットワークに切り替えてください。</string>
|
||||
<string name="disclaimer_title">利用規約</string>
|
||||
<string name="domain_receive_assets_default_address">デフォルトアドレス</string>
|
||||
|
|
@ -569,7 +569,7 @@
|
|||
<string name="give_permission_swap_subtitle" formatted="false">続行するには、%1sスマートコントラクトに%2sを使用する権限を付与してください</string>
|
||||
<string name="give_permission_title">許可を与える</string>
|
||||
<string name="give_permission_unlimited">無制限</string>
|
||||
<string name="hardware_wallet_backup_feature_description">ハードウェアウォレット内に新しいアドレスが自動生成され、安全にすぐ利用できる状態になります。</string>
|
||||
<string name="hardware_wallet_backup_feature_description">アドレスはTangemハードウェアウォレット上で直接生成され、そのまま安全に使用できます。</string>
|
||||
<string name="hardware_wallet_backup_feature_title">新しいアドレス</string>
|
||||
<string name="hardware_wallet_create_title">Tangemウォレットを追加</string>
|
||||
<string name="hardware_wallet_key_feature_description">秘密鍵はTangemカード内部で直接生成され、外部に出ることは一切ありません。</string>
|
||||
|
|
@ -645,7 +645,7 @@
|
|||
<string name="hw_upgrade_error_wallet2_card_required">操作中にエラーが発生しました。</string>
|
||||
<string name="hw_upgrade_funds_access_description">処理中も、資金は安全に保たれ、常にアクセス可能です。</string>
|
||||
<string name="hw_upgrade_funds_access_title">資金へのアクセス</string>
|
||||
<string name="hw_upgrade_general_security_description">アップグレード後、モバイルウォレットはアプリから削除され、ハードウェアウォレットに保存されます。リカバリーフレーズは引き続きご自身で管理してください。</string>
|
||||
<string name="hw_upgrade_general_security_description">アップグレード後、モバイルウォレットはアプリから削除され、Tangemハードウェアウォレットに保存されます。リカバリーフレーズは引き続きお手元に保持されます。</string>
|
||||
<string name="hw_upgrade_general_security_title">セキュリティ全般</string>
|
||||
<string name="hw_upgrade_key_migration_description">秘密鍵はアプリからTangemハードウェアウォレットへ移行されます</string>
|
||||
<string name="hw_upgrade_key_migration_title">キーの移行</string>
|
||||
|
|
@ -984,7 +984,7 @@
|
|||
<string name="onboarding_wallet_info_title_fourth">アクセスコードの復元</string>
|
||||
<string name="onboarding_wallet_info_title_second">同一のカード</string>
|
||||
<string name="onboarding_wallet_info_title_third">アクセスコード</string>
|
||||
<string name="only_one_mobile_wallet_explanation">同時に利用できるのは、モバイルウォレットまたはハードウェアウォレットのいずれか一方のみです。Tangemのコールドウォレットを使用するには、ウォレットをアップグレードするか、アプリから削除してください。</string>
|
||||
<string name="only_one_mobile_wallet_explanation">モバイルウォレットは1つまでしか作成できません。Tangemハードウェアウォレットにアップグレードするか、新しいハードウェアウォレットを追加してください。</string>
|
||||
<string name="onramp_all_offers_button_title">すべてのオファー</string>
|
||||
<string name="onramp_avaiable_with_payment_methods">%sで利用可能</string>
|
||||
<string name="onramp_choose_provider_title_hint">プロバイダーが取引を促進します</string>
|
||||
|
|
@ -1031,6 +1031,7 @@
|
|||
<string name="onramp_transaction_status_footer_text">この画面を閉じて、トークンの詳細画面で取引状況を確認できます。</string>
|
||||
<string name="onramp_up_to_rate">最大</string>
|
||||
<string name="onramp_via">経由</string>
|
||||
<string name="onramp_via_provider">%s 経由</string>
|
||||
<string name="onramp_you_will_pay_title">支払い</string>
|
||||
<string name="organize_tokens_group">グループ</string>
|
||||
<string name="organize_tokens_sort_by_balance">残高順</string>
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@
|
|||
<string name="app_settings_off_saved_wallet_alert_message">При отключении функции сохранения кошелька все ранее сохраненные кошельки будут удалены из приложения.</string>
|
||||
<string name="app_settings_on_require_access_code_alert_message">Это удалит все сохранённые коды доступа к кошелькам. Любое дальнейшее взаимодействие с кошельком будет требовать ввода кода доступа.</string>
|
||||
<string name="app_settings_require_access_code">Требуется код доступа</string>
|
||||
<string name="app_settings_require_access_code_footer">Эта опция отключает использование биометрии для выполнения чувствительных действий. Каждый раз, например при подписании транзакции, вам потребуется вводить код доступа.</string>
|
||||
<string name="app_settings_require_access_code_footer">Эта опция отключает использование биометрии для выполнения чувствительных действий. Каждый раз, например, при подписании транзакции, вам потребуется вводить код доступа.</string>
|
||||
<string name="app_settings_saved_access_codes">Сохранение кода доступа</string>
|
||||
<string name="app_settings_saved_access_codes_footer">Подключите функцию хранения кодов доступа от карт на телефоне в зашифрованном виде, и при работе с картой или кольцом вместо кода доступа будет запрашиваться биометрическая аутентификация.</string>
|
||||
<string name="app_settings_saved_wallet">Cохранение кошелька</string>
|
||||
|
|
@ -737,7 +737,7 @@
|
|||
<string name="markets_loading_no_data_title">Нет данных</string>
|
||||
<string name="markets_pulse_common_title">Пульс рынка</string>
|
||||
<string name="markets_quick_actions">Быстрые действия</string>
|
||||
<string name="markets_search_header_title">Поиск на рынке</string>
|
||||
<string name="markets_search_header_title">Поиск токенов</string>
|
||||
<string name="markets_search_result_title">Результат</string>
|
||||
<string name="markets_search_see_tokens_under_100k">Токены с капитализацией меньше 100к USD</string>
|
||||
<string name="markets_search_show_tokens">Показать токены</string>
|
||||
|
|
@ -1079,6 +1079,7 @@
|
|||
<string name="onramp_transaction_status_footer_text">Вы можете закрыть этот экран и проверить статус транзакции на экране информации о токене.</string>
|
||||
<string name="onramp_up_to_rate">До</string>
|
||||
<string name="onramp_via">Через</string>
|
||||
<string name="onramp_via_provider">через %s</string>
|
||||
<string name="onramp_you_will_pay_title">Вы заплатите</string>
|
||||
<string name="organize_tokens_group">Группы</string>
|
||||
<string name="organize_tokens_sort_by_balance">По балансу</string>
|
||||
|
|
|
|||
|
|
@ -122,10 +122,10 @@
|
|||
<string name="auth_info_subtitle">Виберіть гаманець для входу</string>
|
||||
<string name="auth_info_title">З поверненням!</string>
|
||||
<plurals name="auth_wallet_hardware_description">
|
||||
<item quantity="one">%s картка</item>
|
||||
<item quantity="few">%s картки</item>
|
||||
<item quantity="many">%s карток</item>
|
||||
<item quantity="other">%s картки</item>
|
||||
<item quantity="one">%d картка</item>
|
||||
<item quantity="few">%d картки</item>
|
||||
<item quantity="many">%d карток</item>
|
||||
<item quantity="other">%d картки</item>
|
||||
</plurals>
|
||||
<string name="auth_wallet_mobile_description">Мобільний гаманець</string>
|
||||
<string name="backup_complete_description">Ви успішно створили резервну копію свого гаманця.</string>
|
||||
|
|
@ -177,10 +177,10 @@
|
|||
<item quantity="other">%d пристроїв</item>
|
||||
</plurals>
|
||||
<plurals name="card_label_token_count">
|
||||
<item quantity="one">%1$d токен</item>
|
||||
<item quantity="few">%1$d токени</item>
|
||||
<item quantity="many">%1$d токенів</item>
|
||||
<item quantity="other">%1$d токени</item>
|
||||
<item quantity="one">%d токен</item>
|
||||
<item quantity="few">%d токени</item>
|
||||
<item quantity="many">%d токенів</item>
|
||||
<item quantity="other">%d токени</item>
|
||||
</plurals>
|
||||
<string name="card_reset_alert_continue_message">Будь ласка, скиньте налаштування наступного пристрою, щоб продовжити.</string>
|
||||
<string name="card_reset_alert_continue_title">Скидання гаманця</string>
|
||||
|
|
@ -322,10 +322,10 @@
|
|||
<string name="common_network_fee_title">Комісія мережі</string>
|
||||
<string name="common_network_fee_warning_content">Сума відправлення буде зменшена на %1$s (%2$s) для покриття обраного рівня комісії</string>
|
||||
<plurals name="common_networks_count">
|
||||
<item quantity="one">%1$s мережа</item>
|
||||
<item quantity="few">%1$s мережі</item>
|
||||
<item quantity="many">%1$s мереж</item>
|
||||
<item quantity="other">%1$s мережі</item>
|
||||
<item quantity="one">%d мережа</item>
|
||||
<item quantity="few">%d мережі</item>
|
||||
<item quantity="many">%d мереж</item>
|
||||
<item quantity="other">%d мережі</item>
|
||||
</plurals>
|
||||
<string name="common_new_address">Нова адреса</string>
|
||||
<string name="common_news">Новини</string>
|
||||
|
|
@ -824,16 +824,16 @@
|
|||
<string name="mobile_wallet_requires_min_os_warning_title">Мобільний гаманець потребує %1$s або новіше.</string>
|
||||
<string name="news_all_news">Всі новини</string>
|
||||
<plurals name="news_published_hours_ago">
|
||||
<item quantity="one">%1$s годину тому</item>
|
||||
<item quantity="few">%1$s години тому</item>
|
||||
<item quantity="many">%1$s годин тому</item>
|
||||
<item quantity="other">%1$s годин тому</item>
|
||||
<item quantity="one">%d годину тому</item>
|
||||
<item quantity="few">%d години тому</item>
|
||||
<item quantity="many">%d годин тому</item>
|
||||
<item quantity="other">%d годин тому</item>
|
||||
</plurals>
|
||||
<plurals name="news_published_minutes_ago">
|
||||
<item quantity="one">%1$s хвилину тому</item>
|
||||
<item quantity="few">%1$s хвилини тому</item>
|
||||
<item quantity="many">%1$s хвилин тому</item>
|
||||
<item quantity="other">%1$s хвилин тому</item>
|
||||
<item quantity="one">%d хвилину тому</item>
|
||||
<item quantity="few">%d хвилини тому</item>
|
||||
<item quantity="many">%d хвилин тому</item>
|
||||
<item quantity="other">%d хвилин тому</item>
|
||||
</plurals>
|
||||
<string name="news_related_tokens">Пов\'язані токени</string>
|
||||
<string name="news_stay_in_the_loop">Залишайтеся в курсі подій</string>
|
||||
|
|
@ -1033,10 +1033,10 @@
|
|||
<string name="onramp_provider_max_amount">Доступно до %s</string>
|
||||
<string name="onramp_provider_min_amount">Доступно від %s</string>
|
||||
<plurals name="onramp_providers_count">
|
||||
<item quantity="one">%s провайдер</item>
|
||||
<item quantity="few">%s провайдери</item>
|
||||
<item quantity="many">%s провайдерів</item>
|
||||
<item quantity="other">%s провайдерів</item>
|
||||
<item quantity="one">%d провайдер</item>
|
||||
<item quantity="few">%d провайдери</item>
|
||||
<item quantity="many">%d провайдерів</item>
|
||||
<item quantity="other">%d провайдерів</item>
|
||||
</plurals>
|
||||
<string name="onramp_providers_subtitle">Провайдери</string>
|
||||
<string name="onramp_recently_used_title">Нещодавно використано</string>
|
||||
|
|
@ -1050,10 +1050,10 @@
|
|||
<string name="onramp_settings_title">Параметри</string>
|
||||
<string name="onramp_statuses_view_footer">Ви можете закрити цей екран і перевірити статус транзакції на екрані деталей токена.</string>
|
||||
<plurals name="onramp_timing_days">
|
||||
<item quantity="one">до %s дня</item>
|
||||
<item quantity="few">до %s днів</item>
|
||||
<item quantity="many">до %s днів</item>
|
||||
<item quantity="other">до %s днів</item>
|
||||
<item quantity="one">до %d дня</item>
|
||||
<item quantity="few">до %d днів</item>
|
||||
<item quantity="many">до %d днів</item>
|
||||
<item quantity="other">до %d днів</item>
|
||||
</plurals>
|
||||
<string name="onramp_timing_minutes">%s хв</string>
|
||||
<string name="onramp_title_available_from">Доступно з</string>
|
||||
|
|
@ -1063,6 +1063,7 @@
|
|||
<string name="onramp_transaction_status_footer_text">Ви можете перевірити статус транзакції на сторінці токена</string>
|
||||
<string name="onramp_up_to_rate">До</string>
|
||||
<string name="onramp_via">Через</string>
|
||||
<string name="onramp_via_provider">через %s</string>
|
||||
<string name="onramp_you_will_pay_title">Ви заплатите</string>
|
||||
<string name="organize_tokens_group">Групами</string>
|
||||
<string name="organize_tokens_sort_by_balance">За балансом</string>
|
||||
|
|
@ -1664,10 +1665,10 @@
|
|||
<string name="user_wallet_list_unlock_all">Розблокувати все</string>
|
||||
<string name="user_wallet_list_unlock_all_with">Розблокувати все з %s</string>
|
||||
<plurals name="visa_limits_available_for_days_title">
|
||||
<item quantity="one">доступно протягом %1$s дня</item>
|
||||
<item quantity="few">доступно протягом %1$s днів</item>
|
||||
<item quantity="many">доступно протягом %1$s днів</item>
|
||||
<item quantity="other">доступно протягом %1$s днів</item>
|
||||
<item quantity="one">доступно протягом %d дня</item>
|
||||
<item quantity="few">доступно протягом %d днів</item>
|
||||
<item quantity="many">доступно протягом %d днів</item>
|
||||
<item quantity="other">доступно протягом %d днів</item>
|
||||
</plurals>
|
||||
<string name="visa_main_balances_and_limits">Баланси та ліміти</string>
|
||||
<string name="visa_onboarding_access_code_navigation_title">Код доступу</string>
|
||||
|
|
|
|||
|
|
@ -299,6 +299,7 @@
|
|||
<string name="common_go_to_token">Go to token</string>
|
||||
<string name="common_got_it">Got it</string>
|
||||
<string name="common_hide">Hide</string>
|
||||
<string name="common_hold_to">Hold to %s</string>
|
||||
<string name="common_hour">hour</string>
|
||||
<string name="common_import">Import</string>
|
||||
<string name="common_in_progress">In progress</string>
|
||||
|
|
@ -374,6 +375,7 @@
|
|||
<string name="common_swap">Swap</string>
|
||||
<string name="common_tangem">Tangem</string>
|
||||
<string name="common_tangem_wallet">Tangem Wallet</string>
|
||||
<string name="common_tap_and_hold_hint">Tap and hold</string>
|
||||
<string name="common_terms_and_conditions">terms and conditions</string>
|
||||
<string name="common_terms_of_use">Terms of Use</string>
|
||||
<string name="common_to">To</string>
|
||||
|
|
@ -463,6 +465,11 @@
|
|||
<string name="domain_receive_assets_onboarding_description">Sending assets in other networks will result in permanent loss.</string>
|
||||
<string name="domain_receive_assets_onboarding_network_name">%s network</string>
|
||||
<string name="domain_receive_assets_onboarding_title">Send funds using only</string>
|
||||
<string name="earn_best_opportunities">Best opportunities</string>
|
||||
<string name="earn_filter_all_networks">All networks</string>
|
||||
<string name="earn_filter_all_types">All types</string>
|
||||
<string name="earn_mostly_used">Mostly used</string>
|
||||
<string name="earn_title">Earn</string>
|
||||
<string name="email_preface_wc_error">Hi support team, I\'ve encountered an error with code: %s</string>
|
||||
<string name="email_subject_wc_error">WalletConnect error</string>
|
||||
<string name="error_wrong_wallet_tapped">You have used a card or ring from another wallet. Tap the card or ring associated with this wallet</string>
|
||||
|
|
@ -523,7 +530,7 @@
|
|||
<string name="express_provider">Provider</string>
|
||||
<string name="express_provider_best_rate">Best rate</string>
|
||||
<string name="express_provider_fca_warning_list">FCA Warning List</string>
|
||||
<string name="express_provider_great_rate">Best choice</string>
|
||||
<string name="express_provider_great_rate">Competitive rate</string>
|
||||
<string name="express_provider_in_fca_warning_list">Provider in FCA warning list</string>
|
||||
<string name="express_provider_max_amount">Available up to %s</string>
|
||||
<string name="express_provider_min_amount">Available from %s</string>
|
||||
|
|
@ -725,7 +732,7 @@
|
|||
<string name="markets_loading_no_data_title">No data</string>
|
||||
<string name="markets_pulse_common_title">Market Pulse</string>
|
||||
<string name="markets_quick_actions">Quick actions</string>
|
||||
<string name="markets_search_header_title">Search through the market</string>
|
||||
<string name="markets_search_header_title">Search tokens</string>
|
||||
<string name="markets_search_result_title">Result</string>
|
||||
<string name="markets_search_see_tokens_under_100k">See tokens under 100k USD market cap</string>
|
||||
<string name="markets_search_show_tokens">Show tokens</string>
|
||||
|
|
@ -740,10 +747,10 @@
|
|||
<string name="markets_selector_interval_7d_title">7d</string>
|
||||
<string name="markets_selector_interval_all_title">All</string>
|
||||
<string name="markets_sort_by_experienced_buyers_title">Experienced buyers</string>
|
||||
<string name="markets_sort_by_rating_title">Capitalization</string>
|
||||
<string name="markets_sort_by_rating_title">Market Cap</string>
|
||||
<string name="markets_sort_by_title">Sort By</string>
|
||||
<string name="markets_sort_by_top_gainers_title">Top Gainers</string>
|
||||
<string name="markets_sort_by_top_losers_title">Top Losers</string>
|
||||
<string name="markets_sort_by_top_gainers_title">Gainers</string>
|
||||
<string name="markets_sort_by_top_losers_title">Losers</string>
|
||||
<string name="markets_sort_by_trending_title">Trending</string>
|
||||
<string name="markets_sort_by_yield_mode_title">Yield Mode</string>
|
||||
<string name="markets_staking_banner_description_placeholder">Staking is the easiest way to receive rewards on your crypto. %s</string>
|
||||
|
|
@ -773,7 +780,7 @@
|
|||
<string name="markets_token_details_exchanges_title">Exchanges</string>
|
||||
<string name="markets_token_details_experienced_buyers">Experienced buyers</string>
|
||||
<string name="markets_token_details_experienced_buyers_description">Net buyers with the additional requirement of having at least 100 outgoing transactions</string>
|
||||
<string name="markets_token_details_experienced_buyers_full">Experienced buyers</string>
|
||||
<string name="markets_token_details_experienced_buyers_full">Active Buyers</string>
|
||||
<string name="markets_token_details_fully_diluted_valuation">Fully diluted valuation</string>
|
||||
<string name="markets_token_details_fully_diluted_valuation_description">The total theoretical value of a cryptocurrency if all coins that could exist are in circulation, including those not currently circulating</string>
|
||||
<string name="markets_token_details_fully_diluted_valuation_full">Fully diluted valuation</string>
|
||||
|
|
@ -1017,7 +1024,7 @@
|
|||
<string name="onramp_max_amount_restriction">The purchase amount should be no more than %s</string>
|
||||
<string name="onramp_min_amount_restriction">The amount to buy must be at least %s</string>
|
||||
<string name="onramp_no_available_providers">No available providers for this currency</string>
|
||||
<string name="onramp_offer_type_fastet">Fastest</string>
|
||||
<string name="onramp_offer_type_fastet">Quickest Processing</string>
|
||||
<string name="onramp_pay_with">Pay with</string>
|
||||
<string name="onramp_payment_method_subtitle">Payment method</string>
|
||||
<string name="onramp_provider_max_amount">Available up to %s</string>
|
||||
|
|
@ -1049,6 +1056,7 @@
|
|||
<string name="onramp_transaction_status_footer_text">You can close this screen and check the transaction status on the token details screen.</string>
|
||||
<string name="onramp_up_to_rate">Up to</string>
|
||||
<string name="onramp_via">Via</string>
|
||||
<string name="onramp_via_provider">via %s</string>
|
||||
<string name="onramp_you_will_pay_title">You will pay</string>
|
||||
<string name="organize_tokens_group">Group</string>
|
||||
<string name="organize_tokens_sort_by_balance">By balance</string>
|
||||
|
|
@ -1271,6 +1279,7 @@
|
|||
<string name="staking_amount_tron_integer_error">Staking amount will be rounded to %1$s TRX due to network rules.</string>
|
||||
<string name="staking_amount_tron_integer_error_unstaking">Unstaking amount will be rounded to %1$s TRX due to network rules.</string>
|
||||
<string name="staking_apr_earn_badge">APR %1$s%%</string>
|
||||
<string name="staking_cardano_details_rewards_info_text">Your staking rewards will start after 5 epochs (~25 days) while your delegation is registered and counted by the network.</string>
|
||||
<string name="staking_claim_unstaked">Claim unstaked</string>
|
||||
<string name="staking_details_account_fee">Stake account fee</string>
|
||||
<string name="staking_details_account_fee_info">A staking account is a special account where staked SOL tokens are stored. It is created when you delegate your tokens to a validator to participate in transaction validation and receive rewards. A small fee is charged for creating the staking account, which is returned after the staking is completed.</string>
|
||||
|
|
@ -1603,7 +1612,7 @@
|
|||
<string name="token_swap_changelly_promotion_message">Exchange this token for another at %1$s service fees from February %2$s-%3$s.</string>
|
||||
<string name="token_swap_changelly_promotion_title">Swap with Changelly, %s fees</string>
|
||||
<string name="token_swap_promotion_button">Swap now</string>
|
||||
<string name="tokens_list_hot_crypto_header">Hot crypto 🔥</string>
|
||||
<string name="tokens_list_hot_crypto_header">Market Trend 🔥</string>
|
||||
<string name="tokens_list_unavailable_to_purchase_header">Unavailable to purchase</string>
|
||||
<string name="tokens_list_unavailable_to_sell_header">Unavailable to sell</string>
|
||||
<string name="tokens_list_unavailable_to_swap_header">Unavailable for swap from %s</string>
|
||||
|
|
|
|||
|
|
@ -4,22 +4,18 @@ import com.tangem.data.account.fetcher.DefaultWalletAccountsFetcher.FetchResult
|
|||
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
|
||||
import com.tangem.data.common.currency.UserTokensResponseAccountIdEnricher
|
||||
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.ApiResponseError
|
||||
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.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.account.GetWalletAccountsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -27,26 +23,21 @@ import javax.inject.Singleton
|
|||
/**
|
||||
* 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 userTokensResponseStore provides access to user token responses.
|
||||
* @property defaultWalletAccountsResponseFactory creates [GetWalletAccountsResponse] from [UserTokensResponse]
|
||||
* @property dispatchers dispatchers
|
||||
* @property walletServerBinder binds user wallets on the server
|
||||
*
|
||||
* @see DefaultWalletAccountsFetcher
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@Singleton
|
||||
internal class FetchWalletAccountsErrorHandler @Inject constructor(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val userTokensSaver: UserTokensSaver,
|
||||
private val userTokensResponseStore: UserTokensResponseStore,
|
||||
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? {
|
||||
val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId) ?: return null
|
||||
|
||||
val creationResponse = withContext(dispatchers.io) {
|
||||
tangemTechApi.createWallet(
|
||||
body = WalletIdBodyConverter.convert(userWallet),
|
||||
)
|
||||
}
|
||||
val creationResponse = walletServerBinder.bind(userWalletId)
|
||||
|
||||
return if (creationResponse is ApiResponse.Success && creationResponse.code == Code.CREATED) {
|
||||
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.utils.DefaultWalletAccountsResponseFactory
|
||||
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.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code
|
||||
import com.tangem.datasource.api.common.response.ETAG_HEADER
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
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.WalletAccountDTO
|
||||
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
|
||||
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.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
|
@ -33,18 +32,16 @@ import org.junit.jupiter.api.TestInstance
|
|||
class FetchWalletAccountsErrorHandlerTest {
|
||||
|
||||
private val tangemTechApi: TangemTechApi = mockk()
|
||||
private val userWalletsStore: UserWalletsStore = mockk()
|
||||
private val walletServerBinder: WalletServerBinder = mockk()
|
||||
private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true)
|
||||
private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true)
|
||||
private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory = mockk()
|
||||
|
||||
private val handler = FetchWalletAccountsErrorHandler(
|
||||
tangemTechApi = tangemTechApi,
|
||||
userWalletsStore = userWalletsStore,
|
||||
walletServerBinder = walletServerBinder,
|
||||
userTokensSaver = userTokensSaver,
|
||||
userTokensResponseStore = userTokensResponseStore,
|
||||
defaultWalletAccountsResponseFactory = defaultWalletAccountsResponseFactory,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
private val pushWalletAccounts: suspend (List<WalletAccountDTO>, String) -> GetWalletAccountsResponse =
|
||||
|
|
@ -55,7 +52,7 @@ class FetchWalletAccountsErrorHandlerTest {
|
|||
fun setupEach() {
|
||||
clearMocks(
|
||||
tangemTechApi,
|
||||
userWalletsStore,
|
||||
walletServerBinder,
|
||||
userTokensSaver,
|
||||
userTokensResponseStore,
|
||||
defaultWalletAccountsResponseFactory,
|
||||
|
|
@ -112,22 +109,7 @@ class FetchWalletAccountsErrorHandlerTest {
|
|||
code = Code.CREATED,
|
||||
)
|
||||
|
||||
val walletName = "Wallet"
|
||||
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 { walletServerBinder.bind(userWalletId) } returns apiResponse
|
||||
coEvery { pushWalletAccounts(listOf(accountDTO), eTagValue) } returns savedAccountsResponse
|
||||
|
||||
// Act
|
||||
|
|
@ -142,13 +124,7 @@ class FetchWalletAccountsErrorHandlerTest {
|
|||
// Assert
|
||||
coVerify {
|
||||
userTokensSaver.pushWithRetryer(userWalletId, response = savedAccountsResponse.toUserTokensResponse())
|
||||
tangemTechApi.createWallet(
|
||||
WalletIdBody(
|
||||
walletId = userWalletId.stringValue,
|
||||
name = walletName,
|
||||
walletType = WalletType.COLD,
|
||||
),
|
||||
)
|
||||
walletServerBinder.bind(userWalletId)
|
||||
pushWalletAccounts(listOf(accountDTO), eTagValue)
|
||||
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.tokens.UserTokensBackwardCompatibility
|
||||
import com.tangem.data.common.wallet.WalletServerBinder
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
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.WalletType
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
|
|
@ -27,6 +28,8 @@ class UserTokensSaver(
|
|||
private val userTokensResponseStore: UserTokensResponseStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val addressesEnricher: UserTokensResponseAddressesEnricher,
|
||||
private val walletServerBinder: WalletServerBinder,
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
private val pushTokensRetryerPool: RetryerPool,
|
||||
) {
|
||||
|
|
@ -69,9 +72,13 @@ class UserTokensSaver(
|
|||
|
||||
pushNew(userWallet = userWallet, response = enrichedResponse, onFailSend = onFailSend)
|
||||
} else {
|
||||
val conversionData = appsFlyerStore.get()
|
||||
|
||||
val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher).copy(
|
||||
walletName = userWallet.name,
|
||||
walletType = WalletType.from(userWallet),
|
||||
refcode = conversionData?.refcode,
|
||||
campaign = conversionData?.campaign,
|
||||
)
|
||||
|
||||
pushLegacy(userWalletId = userWalletId, response = enrichedResponse, onFailSend = onFailSend)
|
||||
|
|
@ -114,7 +121,7 @@ class UserTokensSaver(
|
|||
apiResponse.cause.isNetworkError(ApiResponseError.HttpException.Code.NOT_FOUND)
|
||||
|
||||
if (isWalletNotFound) {
|
||||
tangemTechApi.createWallet(body = WalletIdBodyConverter.convert(userWallet)).bind()
|
||||
walletServerBinder.bind(userWallet).bind()
|
||||
|
||||
tangemTechApi.saveTokens(
|
||||
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.quote.DefaultQuotesFetcher
|
||||
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.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
|
|
@ -72,6 +75,8 @@ internal object DataCommonModule {
|
|||
userTokensResponseStore: UserTokensResponseStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
addressesEnricher: UserTokensResponseAddressesEnricher,
|
||||
walletServerBinder: WalletServerBinder,
|
||||
appsFlyerStore: AppsFlyerStore,
|
||||
accountsFeatureToggles: AccountsFeatureToggles,
|
||||
): UserTokensSaver {
|
||||
return UserTokensSaver(
|
||||
|
|
@ -84,6 +89,8 @@ internal object DataCommonModule {
|
|||
pushTokensRetryerPool = RetryerPool(
|
||||
coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default),
|
||||
),
|
||||
walletServerBinder = walletServerBinder,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -98,4 +105,20 @@ internal object DataCommonModule {
|
|||
fun provideETagsStore(appPreferencesStore: AppPreferencesStore): ETagsStore {
|
||||
return DefaultETagsStore(appPreferencesStore = appPreferencesStore)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideWalletServerBinder(
|
||||
userWalletsStore: UserWalletsStore,
|
||||
appsFlyerStore: AppsFlyerStore,
|
||||
tangemTechApi: TangemTechApi,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): WalletServerBinder {
|
||||
return DefaultWalletServerBinder(
|
||||
userWalletsStore = userWalletsStore,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
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.AppsFlyerStore
|
||||
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 appsFlyerStore: AppsFlyerStore,
|
||||
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 = appsFlyerStore.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
|
||||
|
||||
import com.tangem.data.common.wallet.WalletServerBinder
|
||||
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.UserTokensResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.WalletType
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
|
|
@ -27,6 +29,8 @@ class UserTokensSaverTest {
|
|||
private val accountsFeatureToggles = mockk<AccountsFeatureToggles> {
|
||||
every { this@mockk.isFeatureEnabled } returns true
|
||||
}
|
||||
private val walletServerBinder: WalletServerBinder = mockk()
|
||||
private val appsFlyerStore: AppsFlyerStore = mockk()
|
||||
|
||||
private val userTokensSaver: UserTokensSaver = UserTokensSaver(
|
||||
tangemTechApi = tangemTechApi,
|
||||
|
|
@ -34,13 +38,21 @@ class UserTokensSaverTest {
|
|||
userTokensResponseStore = userTokensResponseStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
addressesEnricher = enricher,
|
||||
walletServerBinder = walletServerBinder,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
accountsFeatureToggles = accountsFeatureToggles,
|
||||
pushTokensRetryerPool = mockk(),
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(tangemTechApi, userWalletsStore, userTokensResponseStore, enricher)
|
||||
clearMocks(
|
||||
tangemTechApi,
|
||||
userWalletsStore,
|
||||
userTokensResponseStore,
|
||||
enricher,
|
||||
walletServerBinder,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -109,7 +121,6 @@ class UserTokensSaverTest {
|
|||
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
|
||||
coEvery { enricher(userWalletId, response) } returns enrichedResponse
|
||||
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
|
||||
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.AppsFlyerStore
|
||||
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 appsFlyerStore: AppsFlyerStore = mockk()
|
||||
private val tangemTechApi: TangemTechApi = mockk()
|
||||
private val dispatchers: CoroutineDispatcherProvider = TestingCoroutineDispatcherProvider()
|
||||
|
||||
private val binder = DefaultWalletServerBinder(
|
||||
userWalletsStore = userWalletsStore,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
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, appsFlyerStore, 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 { appsFlyerStore.get() } returns conversionData
|
||||
coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse
|
||||
|
||||
val actual = binder.bind(userWalletId = userWalletId)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(apiResponse)
|
||||
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncOrNull(userWalletId)
|
||||
appsFlyerStore.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) {
|
||||
appsFlyerStore.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 { appsFlyerStore.get() } returns null
|
||||
coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse
|
||||
|
||||
val actual = binder.bind(userWalletId = userWalletId)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(apiResponse)
|
||||
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncOrNull(userWalletId)
|
||||
appsFlyerStore.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 { appsFlyerStore.get() } returns conversionData
|
||||
coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse
|
||||
|
||||
val actual = binder.bind(userWalletId = userWalletId)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(apiResponse)
|
||||
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncOrNull(userWalletId)
|
||||
appsFlyerStore.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 { appsFlyerStore.get() } returns conversionData
|
||||
coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse
|
||||
|
||||
val actual = binder.bind(userWallet = userWallet)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(apiResponse)
|
||||
|
||||
coVerifyOrder {
|
||||
appsFlyerStore.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 { appsFlyerStore.get() } returns null
|
||||
coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse
|
||||
|
||||
val actual = binder.bind(userWallet = userWallet)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(apiResponse)
|
||||
|
||||
coVerifyOrder {
|
||||
appsFlyerStore.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 { appsFlyerStore.get() } returns conversionData
|
||||
coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse
|
||||
|
||||
val actual = binder.bind(userWallet = userWallet)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(apiResponse)
|
||||
|
||||
coVerifyOrder {
|
||||
appsFlyerStore.get()
|
||||
tangemTechApi.createWallet(requestBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,14 +4,16 @@ import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConv
|
|||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.NotificationApplicationCreateBody
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.*
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.store
|
||||
import com.tangem.domain.notifications.models.ApplicationId
|
||||
import com.tangem.domain.notifications.repository.PushNotificationsRepository
|
||||
import com.tangem.domain.notifications.models.NotificationsEligibleNetwork
|
||||
import com.tangem.domain.notifications.repository.PushNotificationsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -19,6 +21,7 @@ internal class DefaultPushNotificationsRepository @Inject constructor(
|
|||
private val tangemTechApi: TangemTechApi,
|
||||
private val appInfoProvider: AppInfoProvider,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : PushNotificationsRepository {
|
||||
|
||||
|
|
@ -32,6 +35,7 @@ internal class DefaultPushNotificationsRepository @Inject constructor(
|
|||
timezone = appInfoProvider.timezone,
|
||||
version = appInfoProvider.appVersion,
|
||||
pushToken = pushToken,
|
||||
appsflyerId = appsFlyerStore.getUID(),
|
||||
),
|
||||
).getOrThrow().appId
|
||||
|
||||
|
|
|
|||
|
|
@ -3,17 +3,20 @@ package com.tangem.data.notifications
|
|||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConverter
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.*
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CryptoNetworkResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.NotificationApplicationCreateBody
|
||||
import com.tangem.datasource.api.tangemTech.models.NotificationApplicationIdResponse
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.domain.notifications.models.ApplicationId
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
|
|
@ -31,10 +34,12 @@ class DefaultPushNotificationsRepositoryTest {
|
|||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
preferencesDataStore = preferencesDataStore,
|
||||
)
|
||||
private val appsFlyerStore: AppsFlyerStore = mockk()
|
||||
private val repository = DefaultPushNotificationsRepository(
|
||||
tangemTechApi = tangemTechApi,
|
||||
appInfoProvider = appInfoProvider,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
|
|
@ -52,6 +57,7 @@ class DefaultPushNotificationsRepositoryTest {
|
|||
coEvery { appInfoProvider.language } returns "en"
|
||||
coEvery { appInfoProvider.appVersion } returns "5.21.1"
|
||||
coEvery { appInfoProvider.timezone } returns "UTC"
|
||||
coEvery { appsFlyerStore.getUID() } returns "UID"
|
||||
coEvery { tangemTechApi.createApplicationId(any()) } returns ApiResponse.Success(
|
||||
expectedAppIdResponse,
|
||||
)
|
||||
|
|
@ -71,6 +77,7 @@ class DefaultPushNotificationsRepositoryTest {
|
|||
timezone = "UTC",
|
||||
version = "5.21.1",
|
||||
pushToken = pushToken,
|
||||
appsflyerId = "UID",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.left
|
||||
import arrow.core.right
|
||||
import com.tangem.data.common.wallet.WalletServerBinder
|
||||
import com.tangem.data.wallets.converters.UserWalletRemoteInfoConverter
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
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.models.*
|
||||
import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO.Status
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
|
|
@ -46,6 +48,8 @@ internal class DefaultWalletsRepository(
|
|||
private val seedPhraseNotificationVisibilityStore: RuntimeStateStore<SeedPhraseNotificationsStatuses>,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val authProvider: AuthProvider,
|
||||
private val walletServerBinder: WalletServerBinder,
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
private val moshi: com.squareup.moshi.Moshi,
|
||||
) : WalletsRepository {
|
||||
|
|
@ -250,13 +254,7 @@ internal class DefaultWalletsRepository(
|
|||
}
|
||||
|
||||
override suspend fun createWallet(userWalletId: UserWalletId) {
|
||||
runCatching(dispatchers.io) {
|
||||
val userWallet = userWalletsStore.getSyncStrict(key = userWalletId)
|
||||
|
||||
tangemTechApi.createWallet(
|
||||
body = WalletIdBodyConverter.convert(userWallet = userWallet),
|
||||
)
|
||||
}
|
||||
walletServerBinder.bind(userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun rejectSeedPhraseSecondNotification(userWalletId: UserWalletId) {
|
||||
|
|
@ -413,10 +411,12 @@ internal class DefaultWalletsRepository(
|
|||
associateApplicationIdWithWallets().getOrThrow()
|
||||
}
|
||||
} else {
|
||||
val conversionData = appsFlyerStore.get()
|
||||
val publicKeys = authProvider.getCardsPublicKeys()
|
||||
val walletsBody = wallets.map { userWallet ->
|
||||
WalletIdBodyConverter.convert(
|
||||
userWallet = userWallet,
|
||||
conversionData = conversionData,
|
||||
publicKeys = if (userWallet is UserWallet.Cold) {
|
||||
publicKeys.filterKeys {
|
||||
userWallet.cardsInWallet.contains(it)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package com.tangem.data.wallets.di
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.common.wallet.WalletServerBinder
|
||||
import com.tangem.data.wallets.DefaultWalletNamesMigrationRepository
|
||||
import com.tangem.data.wallets.DefaultWalletsPromoRepository
|
||||
import com.tangem.data.wallets.DefaultWalletsRepository
|
||||
import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository
|
||||
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.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
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.hot.HotWalletAccessCodeAttemptsRepository
|
||||
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
|
||||
import com.tangem.domain.wallets.repository.WalletsPromoRepository
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Binds
|
||||
|
|
@ -40,6 +44,8 @@ internal object WalletsDataModule {
|
|||
userWalletsStore: UserWalletsStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
authProvider: AuthProvider,
|
||||
walletServerBinder: WalletServerBinder,
|
||||
appsFlyerStore: AppsFlyerStore,
|
||||
accountsFeatureToggles: AccountsFeatureToggles,
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
): WalletsRepository {
|
||||
|
|
@ -50,6 +56,8 @@ internal object WalletsDataModule {
|
|||
seedPhraseNotificationVisibilityStore = RuntimeStateStore(defaultValue = emptyMap()),
|
||||
dispatchers = dispatchers,
|
||||
authProvider = authProvider,
|
||||
walletServerBinder = walletServerBinder,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
accountsFeatureToggles = accountsFeatureToggles,
|
||||
moshi = moshi,
|
||||
)
|
||||
|
|
@ -60,6 +68,22 @@ internal object WalletsDataModule {
|
|||
fun provideMigrateNamesRepository(appPreferencesStore: AppPreferencesStore): WalletNamesMigrationRepository {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import androidx.datastore.core.DataStore
|
|||
import androidx.datastore.preferences.core.Preferences
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
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.response.ApiResponse
|
||||
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.PromocodeActivationResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.WalletResponse
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
|
|
@ -38,12 +40,16 @@ class DefaultWalletsRepositoryTest {
|
|||
)
|
||||
private lateinit var tangemTechApi: TangemTechApi
|
||||
private lateinit var dispatchers: CoroutineDispatcherProvider
|
||||
private lateinit var walletServerBinder: WalletServerBinder
|
||||
private lateinit var appsFlyerStore: AppsFlyerStore
|
||||
|
||||
private val testWalletId = UserWalletId("1234567890abcdef")
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
tangemTechApi = mockk()
|
||||
walletServerBinder = mockk()
|
||||
appsFlyerStore = mockk()
|
||||
dispatchers = TestingCoroutineDispatcherProvider()
|
||||
repository = DefaultWalletsRepository(
|
||||
appPreferencesStore = appPreferenceStore,
|
||||
|
|
@ -52,6 +58,8 @@ class DefaultWalletsRepositoryTest {
|
|||
seedPhraseNotificationVisibilityStore = mockk(),
|
||||
dispatchers = dispatchers,
|
||||
authProvider = mockk(),
|
||||
walletServerBinder = walletServerBinder,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
accountsFeatureToggles = mockk(),
|
||||
moshi = mockk(),
|
||||
)
|
||||
|
|
@ -218,10 +226,14 @@ class DefaultWalletsRepositoryTest {
|
|||
seedPhraseNotificationVisibilityStore = mockk(),
|
||||
dispatchers = dispatchers,
|
||||
authProvider = authProvider,
|
||||
walletServerBinder = walletServerBinder,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
accountsFeatureToggles = accountsFeatureToggles,
|
||||
moshi = mockk(),
|
||||
)
|
||||
|
||||
coEvery { appsFlyerStore.get() } returns null
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.associateApplicationIdWithWallets(
|
||||
eq(applicationId),
|
||||
|
|
|
|||
|
|
@ -52,4 +52,128 @@ sealed class TangemPayAnalyticsEvents(
|
|||
categoryName = "Visa Screen",
|
||||
event = "Button - Visa Swap",
|
||||
)
|
||||
|
||||
class ChooseWalletPopup : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Onboarding",
|
||||
event = "Choose Wallet Popup",
|
||||
)
|
||||
|
||||
class CardSettingsClicked : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Screen",
|
||||
event = "Button - Card Settings",
|
||||
)
|
||||
|
||||
class TermsAndLimitsClicked : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Screen",
|
||||
event = "Button - Terms And Limits",
|
||||
)
|
||||
|
||||
class FreezeCardClicked : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Screen",
|
||||
event = "Button - Freeze Card",
|
||||
)
|
||||
|
||||
class FreezeCardConfirmShown : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Screen",
|
||||
event = "Popup - Freeze Confirmation",
|
||||
)
|
||||
|
||||
class FreezeCardConfirmClicked : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Screen",
|
||||
event = "Button - Freeze Confirmation On Popup",
|
||||
)
|
||||
|
||||
class UnfreezeCardClicked : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Screen",
|
||||
event = "Button - Unfreeze Card",
|
||||
)
|
||||
|
||||
class UnfreezeCardConfirmShown : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Screen",
|
||||
event = "Popup - Unfreeze Confirmation",
|
||||
)
|
||||
|
||||
class UnfreezeCardConfirmClicked : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Screen",
|
||||
event = "Button - Unfreeze Confirmation On Popup",
|
||||
)
|
||||
|
||||
class PinCodeClicked : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Screen",
|
||||
event = "Button - PIN Code",
|
||||
)
|
||||
|
||||
class ChangePinScreenShown : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Screen",
|
||||
event = "Screen - Change PIN",
|
||||
)
|
||||
|
||||
class ChangePinSubmitClicked : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Screen",
|
||||
event = "Button - Set PIN On Change PIN Screen",
|
||||
)
|
||||
|
||||
class ChangePinSuccessShown : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Screen",
|
||||
event = "Message - PIN Setup Success",
|
||||
)
|
||||
|
||||
class CurrentPinShown : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Screen",
|
||||
event = "Popup - Current PIN",
|
||||
)
|
||||
|
||||
class ChangePinOnCurrentPinClicked : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Screen",
|
||||
event = "Button - Change PIN On Current PIN",
|
||||
)
|
||||
|
||||
class ViewCardDetailsClicked : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Screen",
|
||||
event = "Button - View Card Details",
|
||||
)
|
||||
|
||||
class CopyCardNumberClicked : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Screen",
|
||||
event = "Button - Copy Card Number",
|
||||
)
|
||||
|
||||
class CopyCardExpiryClicked : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Screen",
|
||||
event = "Button - Copy Card Expiry",
|
||||
)
|
||||
|
||||
class CopyCardCVVClicked : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Screen",
|
||||
event = "Button - Copy CVV",
|
||||
)
|
||||
|
||||
class AddToWalletClicked : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Screen",
|
||||
event = "Button - Add Card To Wallet",
|
||||
)
|
||||
|
||||
class WithdrawClicked : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Screen",
|
||||
event = "Button - Visa Withdraw",
|
||||
)
|
||||
|
||||
class GoToSupportOnBetaBannerClicked : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Screen",
|
||||
event = "Button - Go To Support On Beta Banner",
|
||||
)
|
||||
|
||||
class TransactionInListClicked(type: String, status: String) : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Screen",
|
||||
event = "Clicked On Transaction In List",
|
||||
params = mapOf(
|
||||
"type" to type,
|
||||
"status" to status,
|
||||
),
|
||||
)
|
||||
|
||||
class SupportOnTransactionPopupClicked : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Screen",
|
||||
event = "Button - Support On Transaction Popup",
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -97,9 +97,12 @@ private fun DisclaimerContent(url: String) {
|
|||
val webViewNavigator = rememberWebViewNavigator()
|
||||
val webViewClient = remember {
|
||||
DisclaimerWebViewClient(
|
||||
onLoadingFinished = {
|
||||
onLoadingFinished = { isWithError ->
|
||||
loadingTimeoutJobHolder.cancel()
|
||||
isLoading = false
|
||||
if (isWithError) {
|
||||
webViewNavigator.loadLocalToS()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import timber.log.Timber
|
|||
*/
|
||||
@Suppress("NullableToStringCall")
|
||||
internal class DisclaimerWebViewClient(
|
||||
private val onLoadingFinished: () -> Unit,
|
||||
private val onLoadingFinished: (isWithError: Boolean) -> Unit,
|
||||
) : AccompanistWebViewClient() {
|
||||
|
||||
private val loadedUrls: MutableMap<String, Boolean> = mutableMapOf()
|
||||
|
|
@ -25,7 +25,6 @@ internal class DisclaimerWebViewClient(
|
|||
super.onPageStarted(view, url, favicon)
|
||||
|
||||
Timber.d("onPageStarted: $url")
|
||||
|
||||
if (url != null) {
|
||||
loadedUrls[url] = false
|
||||
}
|
||||
|
|
@ -35,10 +34,9 @@ internal class DisclaimerWebViewClient(
|
|||
super.onPageFinished(view, url)
|
||||
|
||||
Timber.d("onPageFinished: $url")
|
||||
|
||||
if (url != null && loadedUrls.containsKey(url)) {
|
||||
loadedUrls[url] = true
|
||||
onLoadingFinished()
|
||||
onLoadingFinished(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,7 +49,7 @@ internal class DisclaimerWebViewClient(
|
|||
|
||||
if (url != null && loadedUrls.containsKey(url)) {
|
||||
loadedUrls[url] = true
|
||||
onLoadingFinished()
|
||||
onLoadingFinished(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -252,7 +252,7 @@ private fun PaymentBlockInOffer(paymentMethod: OnrampPaymentMethod, providerName
|
|||
SpacerW(6.dp)
|
||||
|
||||
Text(
|
||||
text = providerName,
|
||||
text = stringResourceSafe(R.string.onramp_via_provider, providerName),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier.testTag(OnrampOffersBlockTestTags.PROVIDER_NAME),
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@ package com.tangem.features.tangempay.entity
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.model.CardDataType
|
||||
import com.tangem.utils.StringsSigns
|
||||
|
||||
internal class TangemPayCardDetailsBlockStateFactory(
|
||||
private val cardNumberEnd: String,
|
||||
private val onReveal: () -> Unit,
|
||||
private val onCopy: (String) -> Unit,
|
||||
private val onCopy: (String, CardDataType) -> Unit,
|
||||
) {
|
||||
|
||||
fun getInitialState() = TangemPayCardDetailsUM(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import kotlinx.collections.immutable.toPersistentList
|
|||
@Suppress("LongParameterList")
|
||||
internal class TangemPayDetailsStateFactory(
|
||||
private val onBack: () -> Unit,
|
||||
private val onOpenMenu: () -> Unit,
|
||||
private val intents: TangemPayDetailIntents,
|
||||
private val cardFrozenState: TangemPayCardFrozenState,
|
||||
private val converter: TangemPayCardFrozenStateConverter,
|
||||
|
|
@ -47,6 +48,7 @@ internal class TangemPayDetailsStateFactory(
|
|||
return TangemPayDetailsUM(
|
||||
topBarConfig = TangemPayDetailsTopBarConfig(
|
||||
onBackClick = onBack,
|
||||
onOpenMenu = onOpenMenu,
|
||||
items = listOfNotNull(
|
||||
TangemPayDetailsTopBarMenuItem(
|
||||
type = TangemPayDetailsTopBarMenuItemType.ChangePin,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
|
||||
internal data class TangemPayDetailsTopBarConfig(
|
||||
val onBackClick: () -> Unit,
|
||||
val onOpenMenu: () -> Unit,
|
||||
val items: ImmutableList<TangemPayDetailsTopBarMenuItem>?,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfi
|
|||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||
import com.tangem.features.tangempay.model.CardDataType
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class TangemPayDetailsUM(
|
||||
|
|
@ -25,7 +26,7 @@ internal data class TangemPayCardDetailsUM(
|
|||
val cvv: String,
|
||||
val buttonText: TextReference = TextReference.EMPTY,
|
||||
val onClick: () -> Unit = {},
|
||||
val onCopy: (String) -> Unit = {},
|
||||
val onCopy: (String, CardDataType) -> Unit,
|
||||
val isHidden: Boolean = true,
|
||||
val isLoading: Boolean = false,
|
||||
val cardFrozenState: TangemPayCardFrozenState,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.tangempay.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -9,6 +10,7 @@ import com.tangem.core.ui.clipboard.ClipboardManager
|
|||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
|
||||
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.TangemPayCardDetailsBlockStateFactory
|
||||
|
|
@ -29,6 +31,7 @@ import com.tangem.utils.transformer.update as transformerUpdate
|
|||
|
||||
private const val SHOW_DETAILS_TIME = 30_000L
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class TangemPayCardDetailsBlockModel @Inject constructor(
|
||||
|
|
@ -38,6 +41,7 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
|
|||
private val clipboardManager: ClipboardManager,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val cardDetailsEventListener: CardDetailsEventListener,
|
||||
private val analytics: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
private val params: TangemPayCardDetailsBlockComponent.Params = paramsContainer.require()
|
||||
|
|
@ -74,6 +78,7 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun revealCardDetails() {
|
||||
analytics.send(TangemPayAnalyticsEvents.ViewCardDetailsClicked())
|
||||
modelScope.launch {
|
||||
uiState.transformerUpdate(
|
||||
transformer = DetailsRevealProgressStateTransformer(onClickHide = ::hideCardDetails),
|
||||
|
|
@ -115,7 +120,17 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun copyData(text: String) {
|
||||
private fun copyData(text: String, type: CardDataType) {
|
||||
val event = when (type) {
|
||||
CardDataType.Number -> TangemPayAnalyticsEvents.CopyCardNumberClicked()
|
||||
CardDataType.Expiry -> TangemPayAnalyticsEvents.CopyCardExpiryClicked()
|
||||
CardDataType.CVV -> TangemPayAnalyticsEvents.CopyCardCVVClicked()
|
||||
}
|
||||
analytics.send(event)
|
||||
clipboardManager.setText(text = text.filterNot { it.isWhitespace() }, isSensitive = true)
|
||||
}
|
||||
}
|
||||
|
||||
internal enum class CardDataType {
|
||||
Number, Expiry, CVV
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.tangempay.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -10,6 +11,7 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.message.ToastMessage
|
||||
import com.tangem.domain.pay.model.SetPinResult
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
|
||||
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.TangemPayChangePinUM
|
||||
|
|
@ -30,17 +32,23 @@ internal class TangemPayChangePinModel @Inject constructor(
|
|||
private val uiMessageSender: UiMessageSender,
|
||||
private val router: Router,
|
||||
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||
private val analytics: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require()
|
||||
val uiState: StateFlow<TangemPayChangePinUM>
|
||||
field = MutableStateFlow(getInitialState())
|
||||
|
||||
init {
|
||||
analytics.send(TangemPayAnalyticsEvents.ChangePinScreenShown())
|
||||
}
|
||||
|
||||
private fun onPinCodeChange(pin: String) {
|
||||
uiState.update(transformer = PinCodeChangeTransformer(newPin = pin))
|
||||
}
|
||||
|
||||
private fun onClickSubmit() {
|
||||
analytics.send(TangemPayAnalyticsEvents.ChangePinSubmitClicked())
|
||||
modelScope.launch {
|
||||
uiState.update { it.copy(submitButtonLoading = true) }
|
||||
val result = try {
|
||||
|
|
@ -59,7 +67,10 @@ internal class TangemPayChangePinModel @Inject constructor(
|
|||
message = ToastMessage(resourceReference(R.string.tangempay_pin_validation_error_message)),
|
||||
)
|
||||
}
|
||||
SetPinResult.SUCCESS -> router.push(TangemPayDetailsInnerRoute.ChangePINSuccess)
|
||||
SetPinResult.SUCCESS -> {
|
||||
analytics.send(TangemPayAnalyticsEvents.ChangePinSuccessShown())
|
||||
router.push(TangemPayDetailsInnerRoute.ChangePINSuccess)
|
||||
}
|
||||
SetPinResult.DECRYPTION_ERROR,
|
||||
SetPinResult.UNKNOWN_ERROR,
|
||||
null,
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
private val cardFrozenStateConverter = TangemPayCardFrozenStateConverter(onUnfreezeClick = ::onClickUnfreezeCard)
|
||||
private val stateFactory = TangemPayDetailsStateFactory(
|
||||
onBack = router::pop,
|
||||
onOpenMenu = ::onOpenMenu,
|
||||
intents = this,
|
||||
cardFrozenState = params.config.cardFrozenState,
|
||||
converter = cardFrozenStateConverter,
|
||||
|
|
@ -127,6 +128,7 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onClickPinCode() {
|
||||
analytics.send(TangemPayAnalyticsEvents.PinCodeClicked())
|
||||
if (!params.config.isPinSet) {
|
||||
router.push(TangemPayDetailsInnerRoute.ChangePIN)
|
||||
} else {
|
||||
|
|
@ -140,14 +142,19 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onClickFreezeCard() {
|
||||
analytics.send(TangemPayAnalyticsEvents.FreezeCardClicked())
|
||||
uiMessageSender.send(TangemPayMessagesFactory.createFreezeCardMessage(onFreezeClicked = ::freezeCard))
|
||||
analytics.send(TangemPayAnalyticsEvents.FreezeCardConfirmShown())
|
||||
}
|
||||
|
||||
override fun onClickUnfreezeCard() {
|
||||
analytics.send(TangemPayAnalyticsEvents.UnfreezeCardClicked())
|
||||
uiMessageSender.send(TangemPayMessagesFactory.createUnfreezeCardMessage(onUnfreezeClicked = ::unfreezeCard))
|
||||
analytics.send(TangemPayAnalyticsEvents.UnfreezeCardConfirmShown())
|
||||
}
|
||||
|
||||
private fun freezeCard() {
|
||||
analytics.send(TangemPayAnalyticsEvents.FreezeCardConfirmClicked())
|
||||
modelScope.launch {
|
||||
val result = try {
|
||||
cardDetailsRepository.freezeCard(userWalletId = params.userWalletId, cardId = params.config.cardId)
|
||||
|
|
@ -186,6 +193,7 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun unfreezeCard() {
|
||||
analytics.send(TangemPayAnalyticsEvents.UnfreezeCardConfirmClicked())
|
||||
modelScope.launch {
|
||||
val result = try {
|
||||
cardDetailsRepository.unfreezeCard(userWalletId = params.userWalletId, cardId = params.config.cardId)
|
||||
|
|
@ -243,6 +251,7 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onClickWithdraw() {
|
||||
analytics.send(TangemPayAnalyticsEvents.WithdrawClicked())
|
||||
val currentBalance = balance
|
||||
val depositAddress = currentBalance?.depositAddress
|
||||
if (currentBalance == null || depositAddress == null) {
|
||||
|
|
@ -336,6 +345,7 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onContactSupportClicked() {
|
||||
analytics.send(TangemPayAnalyticsEvents.GoToSupportOnBetaBannerClicked())
|
||||
modelScope.launch {
|
||||
sendFeedbackEmailUseCase.invoke(
|
||||
type = FeedbackEmailType.Visa.FeatureIsBeta(
|
||||
|
|
@ -356,6 +366,7 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onClickAddToWalletBlock() {
|
||||
analytics.send(TangemPayAnalyticsEvents.AddToWalletClicked())
|
||||
router.push(TangemPayDetailsInnerRoute.AddToWallet)
|
||||
}
|
||||
|
||||
|
|
@ -376,6 +387,10 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
}.saveIn(addToWalletBannerJobHolder)
|
||||
}
|
||||
|
||||
private fun onOpenMenu() {
|
||||
analytics.send(TangemPayAnalyticsEvents.CardSettingsClicked())
|
||||
}
|
||||
|
||||
override fun onClickSwap(data: TangemPayTopUpData) {
|
||||
analytics.send(TangemPayAnalyticsEvents.SwapClicked())
|
||||
bottomSheetNavigation.dismiss()
|
||||
|
|
@ -414,6 +429,7 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
|
||||
override fun onClickChangePin() {
|
||||
bottomSheetNavigation.dismiss()
|
||||
analytics.send(TangemPayAnalyticsEvents.ChangePinOnCurrentPinClicked())
|
||||
router.push(TangemPayDetailsInnerRoute.ChangePIN)
|
||||
}
|
||||
|
||||
|
|
@ -422,6 +438,13 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onTransactionClick(item: TangemPayTxHistoryItem) {
|
||||
val (type, status) = when (item) {
|
||||
is TangemPayTxHistoryItem.Collateral -> "collateral" to "unknown"
|
||||
is TangemPayTxHistoryItem.Fee -> "fee" to "unknown"
|
||||
is TangemPayTxHistoryItem.Payment -> "payment" to "unknown"
|
||||
is TangemPayTxHistoryItem.Spend -> "spend" to item.status.name.lowercase()
|
||||
}
|
||||
analytics.send(TangemPayAnalyticsEvents.TransactionInListClicked(type = type, status = status))
|
||||
bottomSheetNavigation.activate(
|
||||
configuration = TangemPayDetailsNavigation.TransactionDetails(
|
||||
transaction = item,
|
||||
|
|
@ -431,6 +454,7 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onClickTermsAndLimits() {
|
||||
analytics.send(TangemPayAnalyticsEvents.TermsAndLimitsClicked())
|
||||
urlOpener.openUrl(TangemPayConstants.TERMS_AND_LIMITS_LINK)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.tangempay.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -9,6 +10,7 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
|||
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
|
||||
import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryDetailsComponent
|
||||
import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUM
|
||||
import com.tangem.features.tangempay.model.transformers.TangemPayTxHistoryDetailsConverter
|
||||
|
|
@ -17,6 +19,7 @@ import kotlinx.coroutines.flow.*
|
|||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class TangemPayTxHistoryDetailsModel @Inject constructor(
|
||||
|
|
@ -25,6 +28,7 @@ internal class TangemPayTxHistoryDetailsModel @Inject constructor(
|
|||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val balanceHidingSettings: GetBalanceHidingSettingsUseCase,
|
||||
private val analytics: AnalyticsEventHandler,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -61,6 +65,7 @@ internal class TangemPayTxHistoryDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun dispute() {
|
||||
analytics.send(TangemPayAnalyticsEvents.SupportOnTransactionPopupClicked())
|
||||
modelScope.launch {
|
||||
val walletMetaInfo = getWalletMetaInfoUseCase.invoke(params.userWalletId).getOrNull() ?: return@launch
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
package com.tangem.features.tangempay.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
|
||||
import com.tangem.features.tangempay.components.TangemPayViewPinComponent
|
||||
import com.tangem.features.tangempay.entity.TangemPayViewPinUM
|
||||
import com.tangem.features.tangempay.model.transformers.TangemPayViewPinErrorStateTransformer
|
||||
|
|
@ -22,6 +24,7 @@ internal class TangemPayViewPinModel @Inject constructor(
|
|||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||
private val analytics: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<TangemPayViewPinComponent.Params>()
|
||||
|
|
@ -30,6 +33,7 @@ internal class TangemPayViewPinModel @Inject constructor(
|
|||
field = MutableStateFlow(getInitialState())
|
||||
|
||||
init {
|
||||
analytics.send(TangemPayAnalyticsEvents.CurrentPinShown())
|
||||
getCardPin()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ private fun PreviewTangemPayAddToWalletScreen() {
|
|||
numberShort = "*1245",
|
||||
expiry = "••/••",
|
||||
cvv = "•••",
|
||||
onCopy = {},
|
||||
onCopy = { _, _ -> },
|
||||
onClick = {},
|
||||
buttonText = TextReference.Res(R.string.tangempay_card_details_hide_text),
|
||||
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
|
||||
import com.tangem.features.tangempay.model.CardDataType
|
||||
|
||||
private val CustomCardBlockColor = Color(0x1F828282)
|
||||
|
||||
|
|
@ -79,9 +80,9 @@ internal fun TangemPayCard(state: TangemPayCardDetailsUM, modifier: Modifier = M
|
|||
cardNumber = state.number,
|
||||
expiry = state.expiry,
|
||||
cvv = state.cvv,
|
||||
onCopyCardNumber = { state.onCopy(state.number) },
|
||||
onCopyCvv = { state.onCopy(state.cvv) },
|
||||
onCopyExpiry = { state.onCopy(state.expiry) },
|
||||
onCopyCardNumber = { state.onCopy(state.number, CardDataType.Number) },
|
||||
onCopyCvv = { state.onCopy(state.cvv, CardDataType.CVV) },
|
||||
onCopyExpiry = { state.onCopy(state.expiry, CardDataType.Expiry) },
|
||||
onHideDetails = state.onClick,
|
||||
modifier = Modifier.graphicsLayer { rotationY = 180f },
|
||||
)
|
||||
|
|
@ -309,7 +310,7 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide
|
|||
expiry = "",
|
||||
cvv = "",
|
||||
buttonText = resourceReference(R.string.tangempay_card_details_show_details),
|
||||
onCopy = {},
|
||||
onCopy = { _, _ -> },
|
||||
isHidden = true,
|
||||
cardFrozenState = TangemPayCardFrozenState.Frozen,
|
||||
),
|
||||
|
|
@ -321,7 +322,7 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide
|
|||
expiry = "",
|
||||
cvv = "",
|
||||
buttonText = resourceReference(R.string.tangempay_card_details_show_details),
|
||||
onCopy = {},
|
||||
onCopy = { _, _ -> },
|
||||
isHidden = true,
|
||||
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
),
|
||||
|
|
@ -333,7 +334,7 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide
|
|||
expiry = "",
|
||||
cvv = "",
|
||||
buttonText = resourceReference(R.string.tangempay_card_details_show_details),
|
||||
onCopy = {},
|
||||
onCopy = { _, _ -> },
|
||||
isHidden = true,
|
||||
cardFrozenState = TangemPayCardFrozenState.Pending,
|
||||
),
|
||||
|
|
@ -341,7 +342,7 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide
|
|||
isLoading = false,
|
||||
onClick = {},
|
||||
buttonText = resourceReference(R.string.tangempay_card_details_hide_details),
|
||||
onCopy = {},
|
||||
onCopy = { _, _ -> },
|
||||
isHidden = false,
|
||||
number = "1234 5678 9012 3456",
|
||||
numberShort = "*3456",
|
||||
|
|
|
|||
|
|
@ -235,7 +235,12 @@ private fun TangemPayDetailsTopAppBar(config: TangemPayDetailsTopBarConfig, modi
|
|||
title = {},
|
||||
actions = {
|
||||
AnimatedVisibility(visible = config.items != null && config.items.isNotEmpty()) {
|
||||
IconButton(onClick = { showDropdownMenu = true }) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
config.onOpenMenu()
|
||||
showDropdownMenu = true
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_more_vertical_24),
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
|
|
@ -286,7 +291,7 @@ private fun TangemPayDetailsScreenPreview(
|
|||
numberShort = "*1245",
|
||||
expiry = "••/••",
|
||||
cvv = "•••",
|
||||
onCopy = {},
|
||||
onCopy = { _, _ -> },
|
||||
onClick = {},
|
||||
buttonText = TextReference.Res(R.string.tangempay_card_details_hide_text),
|
||||
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
|
|
@ -299,7 +304,7 @@ private fun TangemPayDetailsScreenPreview(
|
|||
private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<TangemPayDetailsUM>(
|
||||
collection = listOf(
|
||||
TangemPayDetailsUM(
|
||||
topBarConfig = TangemPayDetailsTopBarConfig(onBackClick = {}, items = null),
|
||||
topBarConfig = TangemPayDetailsTopBarConfig(onBackClick = {}, onOpenMenu = {}, items = null),
|
||||
pullToRefreshConfig = PullToRefreshConfig(isRefreshing = false, onRefresh = {}),
|
||||
balanceBlockState = TangemPayDetailsBalanceBlockState.Content(
|
||||
actionButtons = persistentListOf(
|
||||
|
|
@ -328,7 +333,7 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<Ta
|
|||
),
|
||||
),
|
||||
TangemPayDetailsUM(
|
||||
topBarConfig = TangemPayDetailsTopBarConfig(onBackClick = {}, items = null),
|
||||
topBarConfig = TangemPayDetailsTopBarConfig(onBackClick = {}, onOpenMenu = {}, items = null),
|
||||
pullToRefreshConfig = PullToRefreshConfig(isRefreshing = false, onRefresh = {}),
|
||||
balanceBlockState = TangemPayDetailsBalanceBlockState.Loading(actionButtons = persistentListOf()),
|
||||
addToWalletBlockState = AddToWalletBlockState({}, {}),
|
||||
|
|
@ -364,7 +369,7 @@ private fun TangemPayDetailsTxHistoryScreenPreview(
|
|||
numberShort = "*1245",
|
||||
expiry = "••/••",
|
||||
cvv = "•••",
|
||||
onCopy = {},
|
||||
onCopy = { _, _ -> },
|
||||
onClick = {},
|
||||
buttonText = TextReference.Res(R.string.tangempay_card_details_hide_text),
|
||||
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ internal class TangemPayOnboardingModel @Inject constructor(
|
|||
if (eligibleWalletsIds.size == 1) {
|
||||
checkCustomerInfo(userWalletId = eligibleWalletsIds[0])
|
||||
} else {
|
||||
analytics.send(TangemPayAnalyticsEvents.ChooseWalletPopup())
|
||||
bottomSheetNavigation.activate(TangemPayOnboardingNavigation.WalletSelector(eligibleWalletsIds))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ internal class WalletModel @Inject constructor(
|
|||
private val singleAccountListSupplier: SingleAccountListSupplier,
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
private val feedFeatureToggle: FeedFeatureToggle,
|
||||
private val bindRefcodeWithWalletUseCase: BindRefcodeWithWalletUseCase,
|
||||
val screenLifecycleProvider: ScreenLifecycleProvider,
|
||||
val innerWalletRouter: InnerWalletRouter,
|
||||
) : Model() {
|
||||
|
|
@ -138,6 +139,11 @@ internal class WalletModel @Inject constructor(
|
|||
enableNotificationsIfNeeded()
|
||||
|
||||
clickIntents.initialize(innerWalletRouter, modelScope)
|
||||
|
||||
modelScope.launch {
|
||||
bindRefcodeWithWalletUseCase.retry()
|
||||
.onLeft { Timber.e("Failed to bind refcode with wallets: $it") }
|
||||
}
|
||||
}
|
||||
|
||||
fun onResume() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.feature.wallet.deeplink
|
||||
|
||||
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.core.analytics.api.AnalyticsEventHandler
|
||||
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.message.DialogMessage
|
||||
import com.tangem.core.ui.message.GlobalLoadingMessage
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.models.network.NetworkStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
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.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.usecase.ActivateBitcoinPromocodeUseCase
|
||||
import com.tangem.domain.wallets.usecase.BindRefcodeWithWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.feature.wallet.deeplink.analytics.PromoActivationAnalytics
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
|
|
@ -44,10 +48,15 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor(
|
|||
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
private val activateBitcoinPromocodeUseCase: ActivateBitcoinPromocodeUseCase,
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
private val bindRefcodeWithWalletUseCase: BindRefcodeWithWalletUseCase,
|
||||
private val analyticsEventsHandler: AnalyticsEventHandler,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : PromoDeeplinkHandler {
|
||||
|
||||
private val refcode: String? = queryParams[DeeplinkConst.REF_KEY]
|
||||
private val campaign: String? = queryParams[DeeplinkConst.CAMPAIGN_KEY]
|
||||
|
||||
init {
|
||||
analyticsEventsHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart())
|
||||
val promoCode = queryParams[PROMO_CODE_KEY].orEmpty()
|
||||
|
|
@ -56,6 +65,8 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor(
|
|||
} else {
|
||||
findSelectedWallet(promoCode)
|
||||
}
|
||||
|
||||
saveAndBindRefcode()
|
||||
}
|
||||
|
||||
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}")
|
||||
|
||||
val cryptoCurrencies = multiWalletCryptoCurrenciesSupplier
|
||||
.getSyncOrNull(
|
||||
MultiWalletCryptoCurrenciesProducer.Params(
|
||||
userWallet
|
||||
.walletId,
|
||||
),
|
||||
)
|
||||
val cryptoCurrencies = multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
|
||||
MultiWalletCryptoCurrenciesProducer.Params(userWallet.walletId),
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
val bitcoinStatus = networkStatuses?.firstOrNull { status ->
|
||||
status.network.id == bitcoinCurrency
|
||||
?.network?.id
|
||||
status.network.id == bitcoinCurrency?.network?.id
|
||||
}
|
||||
Timber.tag(LOG_TAG).d("BitcoinStatus $bitcoinStatus")
|
||||
|
||||
|
|
@ -122,6 +128,7 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor(
|
|||
Timber.tag(LOG_TAG).d(
|
||||
"Start activation promoCode ${promoCode.mask()} address ${bitcoinAddress.mask()}",
|
||||
)
|
||||
|
||||
activatePromoCode(bitcoinAddress = bitcoinAddress, promoCode = promoCode)
|
||||
} else {
|
||||
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)
|
||||
|
||||
appsFlyerStore.storeIfAbsent(value = conversionData)
|
||||
|
||||
bindRefcodeWithWalletUseCase(conversionData)
|
||||
.onLeft { Timber.e("Failed to bind refcode with wallets: $it") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : PromoDeeplinkHandler.Factory {
|
||||
override fun create(
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ internal class WalletWarningsSingleEventSender @Inject constructor(
|
|||
// We check that map contains the first seen wallet (will return null instead false/true otherwise)
|
||||
// and for this wallet we haven't shown the activation bs yet (check that returns false, not true)
|
||||
if (isActivationBottomSheetShown[userWalletId] == false) {
|
||||
if (event.type == WalletActivationBannerType.Warning) {
|
||||
if (event.type == WalletActivationBannerType.Warning && event.isBackupExists.not()) {
|
||||
showFinishActivationBottomSheet(userWalletId)
|
||||
}
|
||||
isActivationBottomSheetShown[userWalletId] = true
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import com.tangem.core.decompose.ui.UiMessageSender
|
|||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
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.errors.ActivatePromoCodeError
|
||||
import com.tangem.domain.wallets.usecase.ActivateBitcoinPromocodeUseCase
|
||||
import com.tangem.domain.wallets.usecase.BindRefcodeWithWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.feature.wallet.deeplink.DefaultPromoDeeplinkHandler
|
||||
import com.tangem.feature.wallet.deeplink.analytics.PromoActivationAnalytics
|
||||
|
|
@ -66,6 +68,12 @@ class DefaultPromoDeeplinkHandlerTest {
|
|||
@MockK
|
||||
private lateinit var analyticsEventHandler: AnalyticsEventHandler
|
||||
|
||||
@MockK
|
||||
private lateinit var bindRefcodeWithWalletUseCase: BindRefcodeWithWalletUseCase
|
||||
|
||||
@MockK
|
||||
private lateinit var appsFlyerStore: AppsFlyerStore
|
||||
|
||||
private lateinit var messages: MutableList<UiMessage>
|
||||
|
||||
@Before
|
||||
|
|
@ -105,6 +113,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
|||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
analyticsEventsHandler = analyticsEventHandler,
|
||||
dispatchers = dispatcherProvider,
|
||||
)
|
||||
|
|
@ -137,6 +147,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
|||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
analyticsEventsHandler = analyticsEventHandler,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
|
@ -168,6 +180,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
|||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
analyticsEventsHandler = analyticsEventHandler,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
|
@ -205,6 +219,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
|||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
analyticsEventsHandler = analyticsEventHandler,
|
||||
dispatchers = dispatcherProvider,
|
||||
)
|
||||
|
|
@ -246,6 +262,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
|||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
analyticsEventsHandler = analyticsEventHandler,
|
||||
dispatchers = dispatcherProvider,
|
||||
)
|
||||
|
|
@ -362,6 +380,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
|||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
analyticsEventsHandler = analyticsEventHandler,
|
||||
dispatchers = dispatcherProvider,
|
||||
)
|
||||
|
|
@ -393,6 +413,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
|||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
analyticsEventsHandler = analyticsEventHandler,
|
||||
dispatchers = dispatcherProvider,
|
||||
)
|
||||
|
|
@ -433,6 +455,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
|||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
analyticsEventsHandler = analyticsEventHandler,
|
||||
dispatchers = dispatcherProvider,
|
||||
)
|
||||
|
|
@ -494,6 +518,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
|||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
analyticsEventsHandler = analyticsEventHandler,
|
||||
dispatchers = dispatcherProvider,
|
||||
)
|
||||
|
|
@ -546,6 +572,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
|||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
analyticsEventsHandler = analyticsEventHandler,
|
||||
dispatchers = dispatcherProvider,
|
||||
)
|
||||
|
|
@ -604,6 +632,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
|||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
analyticsEventsHandler = analyticsEventHandler,
|
||||
dispatchers = dispatcherProvider,
|
||||
)
|
||||
|
|
@ -661,6 +691,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
|||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
analyticsEventsHandler = analyticsEventHandler,
|
||||
dispatchers = dispatcherProvider,
|
||||
)
|
||||
|
|
@ -714,6 +746,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
|||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
analyticsEventsHandler = analyticsEventHandler,
|
||||
dispatchers = dispatcherProvider,
|
||||
)
|
||||
|
|
@ -767,6 +801,8 @@ class DefaultPromoDeeplinkHandlerTest {
|
|||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
bindRefcodeWithWalletUseCase = bindRefcodeWithWalletUseCase,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
analyticsEventsHandler = analyticsEventHandler,
|
||||
dispatchers = dispatcherProvider,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
androidGradlePlugin = "8.10.1"
|
||||
firebaseCrashlytics = "3.0.1"
|
||||
googleServices = "4.4.1"
|
||||
playServices = "18.10.0"
|
||||
playServicesAds = "18.2.0"
|
||||
kotlin = "2.1.10"
|
||||
ksp = "2.1.10-1.0.30"
|
||||
firebasePerf = "1.4.2"
|
||||
|
|
@ -254,6 +256,8 @@ desugar = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desuga
|
|||
googlePlay-review = { module = "com.google.android.play:review", version.ref = "googlePlayReview" }
|
||||
googlePlay-review-ktx = { module = "com.google.android.play:review-ktx", version.ref = "googlePlayReviewKtx" }
|
||||
googlePlay-services-wallet = { module = "com.google.android.gms:play-services-wallet", version.ref = "googlePlayServicesWallet" }
|
||||
googlePlay-services = { module = "com.google.android.gms:play-services-base", version.ref = "playServices" }
|
||||
googlePlay-advertising = { module = "com.google.android.gms:play-services-ads-identifier", version.ref = "playServicesAds" }
|
||||
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
|
||||
hilt-core = { module = "com.google.dagger:hilt-core", version.ref = "hilt" }
|
||||
hilt-kapt = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@
|
|||
# https://github.com/tangem/tangem-sdk-android/
|
||||
# https://github.com/tangem/vico
|
||||
|
||||
|
||||
tangemBlockchainSdk = "releases-5.32-1386"
|
||||
tangemBlockchainSdk = "releases-5.32-1388"
|
||||
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
|
||||
tangemCardSdk = "releases-5.32-574"
|
||||
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue