Updated on 2026-08-14
This commit is contained in:
commit
50fe9e603d
1919 changed files with 94940 additions and 15178 deletions
|
|
@ -28,6 +28,7 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
|||
import androidx.lifecycle.flowWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import arrow.core.getOrElse
|
||||
import com.appsflyer.AppsFlyerLib
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.WEBLINK_KEY
|
||||
import com.tangem.common.routing.deeplink.PayloadToDeeplinkConverter
|
||||
|
|
@ -367,6 +368,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
super.onNewIntent(intent)
|
||||
TangemLogger.i("onNewIntent: data=${intent.data}, extras=${intent.extras?.keySet()}")
|
||||
|
||||
// Warm start: let the AppsFlyer SDK resolve a OneLink delivered while the app is already running.
|
||||
AppsFlyerLib.getInstance().performOnDeepLinking(intent, this)
|
||||
|
||||
val isFromPush = intent.extras?.containsKey(OPENED_FROM_GCM_PUSH) == true
|
||||
if (isFromPush) {
|
||||
analyticsEventsHandler.send(Push.PushNotificationOpened())
|
||||
|
|
|
|||
|
|
@ -13,17 +13,9 @@ class AppsFlyerDeepLinkListener @Inject constructor(
|
|||
|
||||
override fun onDeepLinking(p0: DeepLinkResult) {
|
||||
when (p0.status) {
|
||||
DeepLinkResult.Status.FOUND -> {
|
||||
referralParamsHandler.handleDeeplink(deepLink = p0.deepLink)
|
||||
}
|
||||
DeepLinkResult.Status.NOT_FOUND -> {
|
||||
referralParamsHandler.handleNoDeeplink()
|
||||
TangemLogger.i("No deep link found")
|
||||
}
|
||||
DeepLinkResult.Status.ERROR -> {
|
||||
referralParamsHandler.handleNoDeeplink()
|
||||
TangemLogger.e("Deep link error: ${p0.error}")
|
||||
}
|
||||
DeepLinkResult.Status.FOUND -> referralParamsHandler.handleDeeplink(deepLink = p0.deepLink)
|
||||
DeepLinkResult.Status.NOT_FOUND -> TangemLogger.i("No deep link found")
|
||||
DeepLinkResult.Status.ERROR -> TangemLogger.e("Deep link error: ${p0.error}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.tangem.tap.common.analytics.appsflyer
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.appsflyer.AppsFlyerDeeplink
|
||||
import com.tangem.domain.appsflyer.usecase.ClearAppsFlyerDeeplinkUseCase
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Reactively routes AppsFlyer deep links persisted by [AppsFlyerReferralParamsHandler].
|
||||
*
|
||||
* To add a deep link: add it to [AppsFlyerDeeplink] and a branch in [onDeeplinkPending] (the `when` is exhaustive).
|
||||
*/
|
||||
@Singleton
|
||||
class AppsFlyerDeeplinkRouter @Inject constructor(
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val clearAppsFlyerDeeplinkUseCase: ClearAppsFlyerDeeplinkUseCase,
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
private val appRouter: AppRouter,
|
||||
) {
|
||||
|
||||
fun observe(scope: CoroutineScope, currentRoute: Flow<AppRoute?>) {
|
||||
combine(
|
||||
appsFlyerStore.observeNavigationDeeplink(),
|
||||
currentRoute.distinctUntilChanged(),
|
||||
) { deepLinkValue, route ->
|
||||
if (deepLinkValue != null && route != null) deepLinkValue to route else null
|
||||
}
|
||||
.filterNotNull()
|
||||
.onEach { (deepLinkValue, route) -> onDeeplinkPending(deepLinkValue, route) }
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
private suspend fun onDeeplinkPending(deepLinkValue: String, currentRoute: AppRoute) {
|
||||
when (AppsFlyerDeeplink.from(deepLinkValue)) {
|
||||
AppsFlyerDeeplink.TangemPayMobileOnboarding -> routeTangemPayOnboarding(currentRoute)
|
||||
AppsFlyerDeeplink.Referral -> routeReferral(currentRoute)
|
||||
null -> TangemLogger.i("Ignoring unknown AppsFlyer deep link value: $deepLinkValue")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun routeTangemPayOnboarding(currentRoute: AppRoute) {
|
||||
val isEnabled = featureTogglesManager.isFeatureEnabled(
|
||||
FeatureToggles.AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING,
|
||||
)
|
||||
if (!isEnabled) return
|
||||
|
||||
// Not on an idle entry screen yet: keep the deep link pending, re-evaluate on next route change.
|
||||
if (!isIdleEntryPoint(currentRoute)) return
|
||||
|
||||
if (userWalletsListRepository.userWalletsSync().isNotEmpty()) {
|
||||
TangemLogger.i("[TangemPay][HWO] Routing AppsFlyer deep link to Tangem Pay onboarding")
|
||||
// Authorized: push onto the wallet screen so Back returns to it.
|
||||
appRouter.push(AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.MobileOnboardingDeeplink))
|
||||
} else {
|
||||
TangemLogger.i("[TangemPay][HWO] Routing AppsFlyer deep link to hot wallet onboarding")
|
||||
// Not authorized: open onboarding as the root, skipping Home/stories (original cold-start flow).
|
||||
appRouter.replaceAll(AppRoute.TangemPayHotWalletOnboarding)
|
||||
}
|
||||
// One-shot: consume the deep link once routed so the user isn't forced back here on relaunch.
|
||||
clearAppsFlyerDeeplinkUseCase()
|
||||
}
|
||||
|
||||
private suspend fun routeReferral(currentRoute: AppRoute) {
|
||||
val isEnabled = featureTogglesManager.isFeatureEnabled(
|
||||
FeatureToggles.TWI_1512_HIDE_STORIES_FOR_REFERRAL_ENABLED,
|
||||
)
|
||||
if (!isEnabled) return
|
||||
|
||||
// Referral targets fresh installs: from an idle entry screen, go straight to hot wallet creation
|
||||
// (skips stories). The deep link is NOT cleared here — it stays as referral attribution (read by
|
||||
// IsReferralInstallUseCase, cleared on wallet creation); replaceAll keeps it off the back stack.
|
||||
if (!isIdleEntryPoint(currentRoute)) return
|
||||
if (userWalletsListRepository.userWalletsSync().isNotEmpty()) return
|
||||
|
||||
TangemLogger.i("[Referral] Routing AppsFlyer referral deep link to hot wallet creation")
|
||||
appRouter.replaceAll(AppRoute.CreateWalletStart(mode = AppRoute.CreateWalletStart.Mode.HotWallet))
|
||||
}
|
||||
}
|
||||
|
||||
// Route only from idle entry screens so an in-progress flow (scan, KYC, onboarding…) isn't interrupted.
|
||||
internal fun isIdleEntryPoint(currentRoute: AppRoute?): Boolean =
|
||||
currentRoute is AppRoute.Home || currentRoute is AppRoute.Stories || currentRoute is AppRoute.Wallet
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
package com.tangem.tap.common.analytics.appsflyer
|
||||
|
||||
import com.appsflyer.deeplink.DeepLink
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerDeeplinkSource
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.appsflyer.AppsFlyerDeeplink
|
||||
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -19,8 +18,6 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
|
|||
private val coroutineScope: AppCoroutineScope,
|
||||
) {
|
||||
|
||||
private val deepLinkDeferred = CompletableDeferred<String?>()
|
||||
|
||||
fun handle(params: Map<String?, Any?>) {
|
||||
handle(
|
||||
deepLinkValue = params[DEEP_LINK_VALUE] as? String,
|
||||
|
|
@ -35,40 +32,15 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
|
|||
deepLinkSub1 = deepLink.getStringValue(DEEP_LINK_SUB_1),
|
||||
deepLinkSub2 = deepLink.getStringValue(DEEP_LINK_SUB_2),
|
||||
)
|
||||
deepLinkDeferred.complete(deepLink.deepLinkValue)
|
||||
}
|
||||
|
||||
fun handleNoDeeplink() {
|
||||
deepLinkDeferred.complete(null)
|
||||
}
|
||||
|
||||
suspend fun waitForDeeplink(deeplinkSource: AppsFlyerDeeplinkSource): String? {
|
||||
appsFlyerStore.getDeeplink(deeplinkSource)?.let { return it }
|
||||
|
||||
val expectedValue = when (deeplinkSource) {
|
||||
AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding -> TANGEM_PAY_HOT_WALLET_ONBOARDING_DEEP_LINK_VALUE
|
||||
AppsFlyerDeeplinkSource.Referral -> REFERRAL_DEEP_LINK_VALUE
|
||||
}
|
||||
val resolvedValue = deepLinkDeferred.await().takeIf { it == expectedValue }
|
||||
|
||||
// The deep link may have been persisted to the store while we were awaiting (e.g. from
|
||||
// conversion-data handling, which stores the deep link but doesn't complete the deferred).
|
||||
return resolvedValue ?: appsFlyerStore.getDeeplink(deeplinkSource)
|
||||
}
|
||||
|
||||
private fun handle(deepLinkValue: String?, deepLinkSub1: String?, deepLinkSub2: String?) {
|
||||
TangemLogger.i("AppsFlyer deeplink received: value=$deepLinkValue")
|
||||
when (deepLinkValue) {
|
||||
REFERRAL_DEEP_LINK_VALUE -> handleReferral(deepLinkSub1, deepLinkSub2)
|
||||
TANGEM_PAY_HOT_WALLET_ONBOARDING_DEEP_LINK_VALUE -> handleTangemPayHotWalletOnboarding(deepLinkValue)
|
||||
else -> TangemLogger.i("Ignoring deep link with value: ${deepLinkValue ?: "null"}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTangemPayHotWalletOnboarding(deepLinkValue: String) {
|
||||
coroutineScope.launch {
|
||||
appsFlyerStore.storeDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding, deepLinkValue)
|
||||
TangemLogger.i("[TangemPay][HWO] Deep link stored")
|
||||
when (AppsFlyerDeeplink.from(deepLinkValue)) {
|
||||
AppsFlyerDeeplink.Referral -> handleReferral(deepLinkSub1, deepLinkSub2)
|
||||
AppsFlyerDeeplink.TangemPayMobileOnboarding ->
|
||||
storeNavigationDeeplink(AppsFlyerDeeplink.TangemPayMobileOnboarding.deepLinkValue)
|
||||
null -> TangemLogger.i("Ignoring deep link with value: ${deepLinkValue ?: "null"}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,9 +48,7 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
|
|||
@Suppress("NullableToStringCall")
|
||||
TangemLogger.i("refcode=$deepLinkSub1\ncampaign=$deepLinkSub2")
|
||||
|
||||
coroutineScope.launch {
|
||||
appsFlyerStore.storeDeeplink(AppsFlyerDeeplinkSource.Referral, REFERRAL_DEEP_LINK_VALUE)
|
||||
}
|
||||
storeNavigationDeeplink(AppsFlyerDeeplink.Referral.deepLinkValue)
|
||||
|
||||
if (!isValidParam(deepLinkSub1)) {
|
||||
TangemLogger.e("Deeplink conversion data is invalid")
|
||||
|
|
@ -97,6 +67,13 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
|
|||
return value != null && value.isNotBlank() && !value.equals("null", ignoreCase = true)
|
||||
}
|
||||
|
||||
private fun storeNavigationDeeplink(deepLinkValue: String) {
|
||||
coroutineScope.launch {
|
||||
appsFlyerStore.storeNavigationDeeplink(deepLinkValue)
|
||||
TangemLogger.i("AppsFlyer navigation deep link stored: $deepLinkValue")
|
||||
}
|
||||
}
|
||||
|
||||
private fun storeConversionData(refcode: String, campaign: String?) {
|
||||
coroutineScope.launch {
|
||||
appsFlyerStore.storeIfAbsent(
|
||||
|
|
@ -107,9 +84,6 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
|
|||
|
||||
private companion object {
|
||||
|
||||
const val REFERRAL_DEEP_LINK_VALUE = "referral"
|
||||
const val TANGEM_PAY_HOT_WALLET_ONBOARDING_DEEP_LINK_VALUE = "tpay_mobileonboard"
|
||||
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -36,9 +36,9 @@ class AppsFlyerClient @AssistedInject constructor(
|
|||
setAppId(context.packageName)
|
||||
setDebugLog(true)
|
||||
|
||||
subscribeForDeepLink(appsFlyerDeepLinkListener)
|
||||
|
||||
init(apiKey, tangemAFConversionListener, context)
|
||||
subscribeForDeepLink(appsFlyerDeepLinkListener)
|
||||
setOneLinkCustomDomain(BRANDED_ONELINK_DOMAIN)
|
||||
|
||||
TangemLogger.i("Starting AppsFlyer SDK")
|
||||
start(context, apiKey, InitializationListener)
|
||||
|
|
@ -100,4 +100,9 @@ class AppsFlyerClient @AssistedInject constructor(
|
|||
interface Factory {
|
||||
fun create(apiKey: String): AppsFlyerClient
|
||||
}
|
||||
|
||||
private companion object {
|
||||
// Branded AppsFlyer OneLink domain (matches the join.tangem.com App Link filter in AndroidManifest).
|
||||
const val BRANDED_ONELINK_DOMAIN = "join.tangem.com"
|
||||
}
|
||||
}
|
||||
|
|
@ -34,18 +34,11 @@ class CustomerIoAnalyticsHandler(
|
|||
class Builder : AnalyticsHandlerBuilder {
|
||||
override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? {
|
||||
val cdpApiKey = data.config.customerIoCdpApiKey
|
||||
return if (data.logConfig.isCustomerIoLogEnabled) {
|
||||
CustomerIoAnalyticsHandler(client = CustomerIoLogClient())
|
||||
} else if (!cdpApiKey.isNullOrBlank()) {
|
||||
CustomerIoAnalyticsHandler(
|
||||
client = CustomerIoClient(
|
||||
application = data.application,
|
||||
cdpApiKey = cdpApiKey,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (cdpApiKey.isNullOrBlank()) return null
|
||||
|
||||
return CustomerIoAnalyticsHandler(
|
||||
client = CustomerIoClient(application = data.application, cdpApiKey = cdpApiKey),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
package com.tangem.tap.common.analytics.handlers.customerio
|
||||
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
/**
|
||||
* Log client for Customer.io (used in debug mode).
|
||||
*
|
||||
* Logs all operations to Timber instead of sending them to Customer.io.
|
||||
*/
|
||||
internal class CustomerIoLogClient : CustomerIoAnalyticsClient {
|
||||
|
||||
private var userId: String? = null
|
||||
|
||||
override fun setUserId(userId: String) {
|
||||
this.userId = userId
|
||||
TangemLogger.withTag(CustomerIoAnalyticsHandler.ID).d("identify: userId=$userId")
|
||||
}
|
||||
|
||||
override fun clearUserId() {
|
||||
TangemLogger.withTag(CustomerIoAnalyticsHandler.ID).d("clearIdentify: previous userId=$userId")
|
||||
this.userId = null
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,17 @@
|
|||
package com.tangem.tap.data
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultAppInfoProvider @Inject constructor() : AppInfoProvider {
|
||||
internal class DefaultAppInfoProvider @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : AppInfoProvider {
|
||||
override val platform: String
|
||||
get() = "Android"
|
||||
override val device: String
|
||||
|
|
@ -18,6 +22,8 @@ internal class DefaultAppInfoProvider @Inject constructor() : AppInfoProvider {
|
|||
get() = Build.VERSION.SDK_INT
|
||||
override val language: String
|
||||
get() = Locale.getDefault().toLanguageTag()
|
||||
override val deviceScale: Float
|
||||
get() = context.resources.displayMetrics.density
|
||||
override val timezone: String
|
||||
get() = TimeZone.getDefault().id
|
||||
override val appVersion: String = BuildConfig.VERSION_NAME
|
||||
|
|
|
|||
|
|
@ -4,15 +4,16 @@ import androidx.datastore.core.DataStore
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.offramp.model.PendingOfframp
|
||||
import com.tangem.domain.offramp.model.PendingOfframp.Companion.EXPIRY_MS
|
||||
import com.tangem.domain.offramp.repository.OfframpRepository
|
||||
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
|
||||
import com.tangem.tap.data.converter.PendingOfframpEntryConverter
|
||||
import com.tangem.tap.data.model.PendingOfframpEntry
|
||||
import com.tangem.tap.network.exchangeServices.SellService
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Default implementation of [OfframpRepository].
|
||||
|
|
@ -27,7 +28,7 @@ internal class DefaultOfframpRepository(
|
|||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : OfframpRepository {
|
||||
|
||||
private val pendingOfframpConverter = PendingOfframpEntryConverter()
|
||||
private val converter = PendingOfframpEntryConverter()
|
||||
|
||||
override fun getOfframpUrl(
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
|
|
@ -71,13 +72,17 @@ internal class DefaultOfframpRepository(
|
|||
entry.requestId == requestId &&
|
||||
entry.userWalletId == userWalletId.stringValue &&
|
||||
entry.currencyId == currencyId &&
|
||||
now - entry.createdAt < EXPIRY_MS
|
||||
!entry.isExpired(now)
|
||||
}
|
||||
// Keep the matched record so the same redirect can be followed again until it expires; only prune the
|
||||
// expired ones. The record is dropped naturally once it ages past EXPIRY_MS.
|
||||
stored.filterNotExpired(now)
|
||||
}
|
||||
matched?.let(pendingOfframpConverter::convert)
|
||||
matched?.let(converter::convert)
|
||||
}
|
||||
|
||||
override suspend fun getAllStoredOfframps(): List<PendingOfframp> = withContext(dispatchers.io) {
|
||||
pendingOfframpStore.data.first().map(converter::convert)
|
||||
}
|
||||
|
||||
// Returns the same instance when nothing is expired, so DataStore.updateData sees an unchanged value and skips
|
||||
|
|
@ -85,7 +90,5 @@ internal class DefaultOfframpRepository(
|
|||
private fun List<PendingOfframpEntry>.filterNotExpired(now: Long): List<PendingOfframpEntry> =
|
||||
if (none { now - it.createdAt >= EXPIRY_MS }) this else filter { now - it.createdAt < EXPIRY_MS }
|
||||
|
||||
private companion object {
|
||||
val EXPIRY_MS: Long = TimeUnit.HOURS.toMillis(1)
|
||||
}
|
||||
private fun PendingOfframpEntry.isExpired(now: Long): Boolean = converter.convert(this).isExpired(now)
|
||||
}
|
||||
|
|
@ -1,288 +0,0 @@
|
|||
package com.tangem.tap.data
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.tangem.data.pay.entity.WithdrawStoreData
|
||||
import com.tangem.data.pay.util.WithdrawStateConverter
|
||||
import com.tangem.data.pay.util.WithdrawStoreDataConverter
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.store
|
||||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayWithdrawState
|
||||
import com.tangem.domain.visa.model.TangemPayAuthTokens
|
||||
import com.tangem.sdk.storage.AndroidSecureStorageV2
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val AUTH_TOKENS_DEFAULT_KEY = "tangem_pay_default_key"
|
||||
private const val WITHDRAW_ORDER_ID_KEY = "tangem_pay_withdraw_order_id_key"
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
@Singleton
|
||||
internal class DefaultTangemPayStorage @Inject constructor(
|
||||
@ApplicationContext applicationContext: Context,
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
private val dispatcherProvider: CoroutineDispatcherProvider,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : TangemPayStorage {
|
||||
|
||||
private val secureStorage by lazy {
|
||||
AndroidSecureStorageV2(
|
||||
appContext = applicationContext,
|
||||
useStrongBox = false,
|
||||
name = "tangem_pay_storage",
|
||||
)
|
||||
}
|
||||
|
||||
private val tokensAdapter by lazy { moshi.adapter(TangemPayAuthTokens::class.java) }
|
||||
private val withdrawStoreDataConverter by lazy { WithdrawStoreDataConverter() }
|
||||
private val withdrawStateConverter by lazy { WithdrawStateConverter() }
|
||||
|
||||
private val listType by lazy {
|
||||
Types.newParameterizedType(List::class.java, WithdrawStoreData::class.java)
|
||||
}
|
||||
private val mapType by lazy {
|
||||
Types.newParameterizedType(Map::class.java, String::class.java, listType)
|
||||
}
|
||||
private val adapter: JsonAdapter<Map<String, List<WithdrawStoreData>>> by lazy {
|
||||
appPreferencesStore.moshi.adapter(mapType)
|
||||
}
|
||||
|
||||
override suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) {
|
||||
appPreferencesStore
|
||||
.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), customerWalletAddress)
|
||||
}
|
||||
|
||||
override suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String? {
|
||||
return appPreferencesStore.getSyncOrNull(
|
||||
key = PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId),
|
||||
)
|
||||
.takeIf { !it.isNullOrEmpty() }
|
||||
}
|
||||
|
||||
override suspend fun clearCustomerWalletAddress(userWalletId: UserWalletId) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "")
|
||||
}
|
||||
|
||||
override suspend fun storeAuthTokens(customerWalletAddress: String, tokens: TangemPayAuthTokens) =
|
||||
withContext(dispatcherProvider.io) {
|
||||
val json = tokensAdapter.toJson(tokens)
|
||||
|
||||
secureStorage.store(
|
||||
json.encodeToByteArray(throwOnInvalidSequence = true),
|
||||
createAuthTokensKey(customerWalletAddress),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getAuthTokens(customerWalletAddress: String): TangemPayAuthTokens? {
|
||||
return withContext(dispatcherProvider.io) {
|
||||
val authTokens = secureStorage.get(createAuthTokensKey(customerWalletAddress))
|
||||
?.decodeToString(throwOnInvalidSequence = true)
|
||||
?.let(tokensAdapter::fromJson)
|
||||
|
||||
authTokens?.let { tokens ->
|
||||
if (tokens.idempotencyKey == null) {
|
||||
val newAuthTokens = tokens.copy(idempotencyKey = UUID.randomUUID().toString())
|
||||
storeAuthTokens(customerWalletAddress, newAuthTokens)
|
||||
newAuthTokens
|
||||
} else {
|
||||
tokens
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clearAuthTokens(customerWalletAddress: String) {
|
||||
withContext(dispatcherProvider.io) {
|
||||
secureStorage.delete(createAuthTokensKey(customerWalletAddress))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun storeOrderId(customerWalletAddress: String, orderId: String) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), orderId)
|
||||
}
|
||||
|
||||
override suspend fun getOrderId(customerWalletAddress: String): String? {
|
||||
return appPreferencesStore.getSyncOrNull(key = PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress))
|
||||
.takeIf { !it.isNullOrEmpty() }
|
||||
}
|
||||
|
||||
override suspend fun getAddToWalletDone(customerWalletAddress: String): Boolean {
|
||||
return appPreferencesStore.getSyncOrNull(
|
||||
key = PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress),
|
||||
) == true
|
||||
}
|
||||
|
||||
override suspend fun storeAddToWalletDone(customerWalletAddress: String, isDone: Boolean) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), isDone)
|
||||
}
|
||||
|
||||
override suspend fun clearOrderId(customerWalletAddress: String) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "")
|
||||
}
|
||||
|
||||
override suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId, isPaeraCustomer: Boolean) {
|
||||
appPreferencesStore.store(
|
||||
PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId),
|
||||
isPaeraCustomer,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? {
|
||||
return appPreferencesStore.getSyncOrNull(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId))
|
||||
}
|
||||
|
||||
override suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String) {
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
val orders = mutablePreferences.getObjectMap<String>(
|
||||
PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
|
||||
)
|
||||
.plus(createWithdrawOrderIdKey(userWalletId) to orderId)
|
||||
mutablePreferences.setObjectMap(
|
||||
key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
|
||||
value = orders,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) {
|
||||
appPreferencesStore.editData { prefs ->
|
||||
val walletKey = createWithdrawOrderIdKey(userWalletId)
|
||||
val currentMap = prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY]
|
||||
?.let(adapter::fromJson)
|
||||
.orEmpty()
|
||||
val newItem = withdrawStoreDataConverter.convert(data)
|
||||
val currentList = currentMap[walletKey].orEmpty()
|
||||
val updatedList = buildList(currentList.size + 1) {
|
||||
for (item in currentList) { if (item.orderId != newItem.orderId) add(item) }
|
||||
add(newItem)
|
||||
}
|
||||
prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] =
|
||||
adapter.toJson(currentMap + (walletKey to updatedList))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String? {
|
||||
val orders = appPreferencesStore.getObjectMapSync<String>(
|
||||
PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
|
||||
)
|
||||
return orders[createWithdrawOrderIdKey(userWalletId)]
|
||||
}
|
||||
|
||||
override suspend fun getWithdrawOrders(userWalletId: UserWalletId): List<TangemPayWithdrawState> {
|
||||
val map = appPreferencesStore.data.firstOrNull()
|
||||
?.get(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY)?.let(adapter::fromJson).orEmpty()
|
||||
return map[createWithdrawOrderIdKey(userWalletId)].orEmpty().map(withdrawStateConverter::convert)
|
||||
}
|
||||
|
||||
override suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId) {
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
val orders = mutablePreferences.getObjectMap<String>(
|
||||
PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
|
||||
)
|
||||
.minus(createWithdrawOrderIdKey(userWalletId))
|
||||
mutablePreferences.setObjectMap(
|
||||
key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
|
||||
value = orders,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId, orderId: String) {
|
||||
appPreferencesStore.editData { prefs ->
|
||||
val walletKey = createWithdrawOrderIdKey(userWalletId)
|
||||
val currentMap = prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY]?.let(adapter::fromJson)
|
||||
.orEmpty()
|
||||
val currentList = currentMap[walletKey].orEmpty()
|
||||
val updatedList = buildList(currentList.size) {
|
||||
for (item in currentList) {
|
||||
if (item.orderId != orderId) add(item)
|
||||
}
|
||||
}
|
||||
val updatedMap = if (updatedList.isEmpty()) {
|
||||
currentMap - walletKey
|
||||
} else {
|
||||
currentMap + (walletKey to updatedList)
|
||||
}
|
||||
prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] = adapter.toJson(updatedMap)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), hide)
|
||||
}
|
||||
|
||||
override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean {
|
||||
return appPreferencesStore.getSyncOrNull(
|
||||
key = PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId),
|
||||
) == true
|
||||
}
|
||||
|
||||
override suspend fun storeTangemPayEligibility(eligibility: Set<String>) {
|
||||
withContext(dispatcherProvider.io) {
|
||||
appPreferencesStore.store(
|
||||
key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY,
|
||||
value = eligibility,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTangemPayEligibility(): Set<String> {
|
||||
return withContext(dispatcherProvider.io) {
|
||||
appPreferencesStore.getSyncOrDefault(
|
||||
key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY,
|
||||
default = emptySet(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun storeIsTangemPayDeactivated(userWalletId: UserWalletId) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayDeactivatedKey(userWalletId), true)
|
||||
}
|
||||
|
||||
override suspend fun isTangemPayDeactivated(userWalletId: UserWalletId): Boolean {
|
||||
val key = PreferencesKeys.getTangemPayDeactivatedKey(userWalletId)
|
||||
return appPreferencesStore.getSyncOrNull(key) == true
|
||||
}
|
||||
|
||||
override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) {
|
||||
withContext(dispatcherProvider.io) {
|
||||
secureStorage.delete(createAuthTokensKey(customerWalletAddress))
|
||||
}
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), false)
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "")
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "")
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false)
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), false)
|
||||
// Clear the withdraw order hints together with the rest of the cache.
|
||||
deleteActiveWithdrawOrder(userWalletId)
|
||||
clearWithdrawOrders(userWalletId)
|
||||
}
|
||||
|
||||
private suspend fun clearWithdrawOrders(userWalletId: UserWalletId) {
|
||||
appPreferencesStore.editData { prefs ->
|
||||
val walletKey = createWithdrawOrderIdKey(userWalletId)
|
||||
val currentMap = prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY]?.let(adapter::fromJson)
|
||||
.orEmpty()
|
||||
val updatedMap = currentMap - walletKey
|
||||
prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] = adapter.toJson(updatedMap)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAuthTokensKey(address: String): String = "${AUTH_TOKENS_DEFAULT_KEY}_$address"
|
||||
|
||||
private fun createWithdrawOrderIdKey(userWalletId: UserWalletId): String = "${WITHDRAW_ORDER_ID_KEY}_$userWalletId"
|
||||
}
|
||||
|
|
@ -10,8 +10,11 @@ import com.tangem.sdk.api.TangemSdkManager
|
|||
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
|
||||
import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager
|
||||
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
|
||||
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateVirtualAccountAddressTask
|
||||
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
|
||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||
import com.tangem.tap.domain.walletregistration.WalletRegistrationLauncher
|
||||
import dagger.Lazy
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -31,9 +34,11 @@ internal class TangemSdkManagerModule {
|
|||
visaCardScanHandler: VisaCardScanHandler,
|
||||
visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
|
||||
tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
|
||||
tangemPayVirtualAccountTaskFactory: TangemPayGenerateVirtualAccountAddressTask.Factory,
|
||||
onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
|
||||
analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
cardRepository: CardRepository,
|
||||
walletRegistrationLauncher: Lazy<WalletRegistrationLauncher>,
|
||||
): TangemSdkManager {
|
||||
return if (BuildConfig.MOCK_DATA_SOURCE) {
|
||||
MockTangemSdkManager(resources = context.resources)
|
||||
|
|
@ -44,9 +49,11 @@ internal class TangemSdkManagerModule {
|
|||
visaCardScanHandler = visaCardScanHandler,
|
||||
visaCardActivationTaskFactory = visaCardActivationTaskFactory,
|
||||
tangemPayChallengeTaskFactory = tangemPayChallengeTaskFactory,
|
||||
tangemPayVirtualAccountTaskFactory = tangemPayVirtualAccountTaskFactory,
|
||||
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
|
||||
analyticsErrorHandler = analyticsErrorHandler,
|
||||
cardRepository = cardRepository,
|
||||
walletRegistrationLauncher = walletRegistrationLauncher,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import com.tangem.core.navigation.deeplink.DeeplinkLauncher
|
|||
import com.tangem.core.navigation.finisher.AppFinisher
|
||||
import com.tangem.core.navigation.settings.SettingsManager
|
||||
import com.tangem.core.navigation.share.ShareManager
|
||||
import com.tangem.core.navigation.url.AppStoreOpener
|
||||
import com.tangem.core.navigation.url.DefaultAppStoreOpener
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.tap.common.finisher.AndroidAppFinisher
|
||||
|
|
@ -35,6 +37,10 @@ internal interface UtilsModule {
|
|||
@Singleton
|
||||
fun bindAppInfoProvider(impl: DefaultAppInfoProvider): AppInfoProvider
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindAppStoreOpener(impl: DefaultAppStoreOpener): AppStoreOpener
|
||||
|
||||
companion object {
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -1,12 +1,20 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.addressbook.crypto.AddressBookCipher
|
||||
import com.tangem.domain.addressbook.interactor.GetVerifiedContactsInteractor
|
||||
import com.tangem.domain.addressbook.interactor.SaveContactInteractor
|
||||
import com.tangem.domain.addressbook.repository.AddressBookRepository
|
||||
import com.tangem.domain.addressbook.time.DefaultIsoTimestampProvider
|
||||
import com.tangem.domain.addressbook.time.IsoTimestampProvider
|
||||
import com.tangem.domain.addressbook.usecase.ValidateContactAddressUseCase
|
||||
import com.tangem.domain.addressbook.usecase.VerifyAddressEntriesUseCase
|
||||
import com.tangem.domain.tokens.GetNetworkAddressesUseCase
|
||||
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
|
||||
import com.tangem.domain.addressbook.usecase.CheckAddressDuplicateUseCase
|
||||
import com.tangem.domain.addressbook.usecase.DeleteContactUseCase
|
||||
import com.tangem.domain.addressbook.usecase.GetContactByIdUseCase
|
||||
import com.tangem.domain.addressbook.usecase.GetContactsUseCase
|
||||
import com.tangem.domain.addressbook.usecase.SyncAddressBooksUseCase
|
||||
import com.tangem.domain.addressbook.validation.ContactNameValidator
|
||||
import com.tangem.domain.addressbook.verification.ContactSignatureVerifier
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.transaction.usecase.SignUseCase
|
||||
import com.tangem.domain.transaction.usecase.VerifySecp256k1MessagesUseCase
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -20,22 +28,84 @@ object AddressBookDomainModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideValidateContactAddressUseCase(
|
||||
validateWalletAddressUseCase: ValidateWalletAddressUseCase,
|
||||
getNetworkAddressesUseCase: GetNetworkAddressesUseCase,
|
||||
): ValidateContactAddressUseCase {
|
||||
return ValidateContactAddressUseCase(
|
||||
validateWalletAddressUseCase = validateWalletAddressUseCase,
|
||||
getNetworkAddressesUseCase = getNetworkAddressesUseCase,
|
||||
fun provideContactSignatureVerifier(
|
||||
verifyMessagesUseCase: VerifySecp256k1MessagesUseCase,
|
||||
userWalletsListRepository: UserWalletsListRepository,
|
||||
): ContactSignatureVerifier {
|
||||
return ContactSignatureVerifier(
|
||||
verifyMessages = verifyMessagesUseCase,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideVerifyAddressEntriesUseCase(
|
||||
verifyMessagesUseCase: VerifySecp256k1MessagesUseCase,
|
||||
): VerifyAddressEntriesUseCase {
|
||||
return VerifyAddressEntriesUseCase(verifyMessagesUseCase = verifyMessagesUseCase)
|
||||
fun provideContactNameValidator(
|
||||
repository: AddressBookRepository,
|
||||
contactSignatureVerifier: ContactSignatureVerifier,
|
||||
): ContactNameValidator {
|
||||
return ContactNameValidator(
|
||||
repository = repository,
|
||||
contactSignatureVerifier = contactSignatureVerifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetContactsUseCase(repository: AddressBookRepository): GetContactsUseCase {
|
||||
return GetContactsUseCase(repository = repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetVerifiedContactsInteractor(
|
||||
getContactsUseCase: GetContactsUseCase,
|
||||
contactSignatureVerifier: ContactSignatureVerifier,
|
||||
): GetVerifiedContactsInteractor {
|
||||
return GetVerifiedContactsInteractor(
|
||||
getContacts = getContactsUseCase,
|
||||
contactSignatureVerifier = contactSignatureVerifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSaveContactInteractor(
|
||||
repository: AddressBookRepository,
|
||||
contactNameValidator: ContactNameValidator,
|
||||
signUseCase: SignUseCase,
|
||||
timestampProvider: IsoTimestampProvider,
|
||||
): SaveContactInteractor {
|
||||
return SaveContactInteractor(
|
||||
repository = repository,
|
||||
validateContactName = contactNameValidator,
|
||||
signUseCase = signUseCase,
|
||||
timestampProvider = timestampProvider,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDeleteContactUseCase(repository: AddressBookRepository): DeleteContactUseCase {
|
||||
return DeleteContactUseCase(repository = repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetContactByIdUseCase(repository: AddressBookRepository): GetContactByIdUseCase {
|
||||
return GetContactByIdUseCase(repository = repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCheckAddressDuplicateUseCase(repository: AddressBookRepository): CheckAddressDuplicateUseCase {
|
||||
return CheckAddressDuplicateUseCase(repository = repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSyncAddressBooksUseCase(repository: AddressBookRepository): SyncAddressBooksUseCase {
|
||||
return SyncAddressBooksUseCase(repository = repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -56,6 +56,14 @@ internal object ManageTokensDomainModule {
|
|||
return ValidateDerivationPathUseCase(customTokensRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCheckDerivationPathSupportedUseCase(
|
||||
customTokensRepository: CustomTokensRepository,
|
||||
): CheckDerivationPathSupportedUseCase {
|
||||
return CheckDerivationPathSupportedUseCase(customTokensRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCheckCurrencyUnsupportedUseCase(repository: ManageTokensRepository): CheckCurrencyUnsupportedUseCase {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.marketing.DismissMarketingBannerUseCase
|
||||
import com.tangem.domain.marketing.GetMarketingBannerUseCase
|
||||
import com.tangem.domain.marketing.MarketingFeatureToggles
|
||||
import com.tangem.domain.marketing.MarketingRepository
|
||||
import com.tangem.domain.marketing.WarmUpMarketingCampaignsUseCase
|
||||
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 MarketingDomainModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetMarketingBannerUseCase(
|
||||
repository: MarketingRepository,
|
||||
featureToggles: MarketingFeatureToggles,
|
||||
): GetMarketingBannerUseCase = GetMarketingBannerUseCase(repository, featureToggles)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDismissMarketingBannerUseCase(repository: MarketingRepository): DismissMarketingBannerUseCase =
|
||||
DismissMarketingBannerUseCase(repository)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideWarmUpMarketingCampaignsUseCase(
|
||||
repository: MarketingRepository,
|
||||
featureToggles: MarketingFeatureToggles,
|
||||
): WarmUpMarketingCampaignsUseCase = WarmUpMarketingCampaignsUseCase(repository, featureToggles)
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.promo.PromoRepository
|
||||
import com.tangem.domain.promo.usecase.EnrollPromoCampaignUseCase
|
||||
import com.tangem.domain.promo.usecase.GetPromoCampaignStateUseCase
|
||||
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 PromoDomainModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetPromoCampaignStateUseCase(repository: PromoRepository): GetPromoCampaignStateUseCase {
|
||||
return GetPromoCampaignStateUseCase(repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideEnrollPromoCampaignUseCase(repository: PromoRepository): EnrollPromoCampaignUseCase {
|
||||
return EnrollPromoCampaignUseCase(repository)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.pushnotificationpreferences.IsPushNotificationFirstActivationDoneUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.MarkPushNotificationFirstActivationDoneUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.ObserveWalletPushNotificationPreferencesUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.PreloadWalletPushNotificationPreferencesUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.SetAllWalletPushNotificationPreferencesUseCase
|
||||
|
|
@ -46,4 +48,20 @@ internal object PushNotificationPreferencesDomainModule {
|
|||
): SetAllWalletPushNotificationPreferencesUseCase {
|
||||
return SetAllWalletPushNotificationPreferencesUseCase(repository = repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesIsPushNotificationFirstActivationDoneUseCase(
|
||||
repository: WalletPushNotificationPreferencesRepository,
|
||||
): IsPushNotificationFirstActivationDoneUseCase {
|
||||
return IsPushNotificationFirstActivationDoneUseCase(repository = repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesMarkPushNotificationFirstActivationDoneUseCase(
|
||||
repository: WalletPushNotificationPreferencesRepository,
|
||||
): MarkPushNotificationFirstActivationDoneUseCase {
|
||||
return MarkPushNotificationFirstActivationDoneUseCase(repository = repository)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,13 +4,11 @@ import com.tangem.domain.swap.SwapErrorResolver
|
|||
import com.tangem.domain.swap.SwapRepositoryV2
|
||||
import com.tangem.domain.swap.SwapTransactionRepository
|
||||
import com.tangem.domain.swap.usecase.*
|
||||
import com.tangem.feature.swap.domain.GetAvailablePairsUseCase
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
import com.tangem.feature.swap.domain.api.SwapRepository as OldSwapRepository
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -19,12 +17,6 @@ import com.tangem.feature.swap.domain.api.SwapRepository as OldSwapRepository
|
|||
@InstallIn(SingletonComponent::class)
|
||||
internal object SwapDomainModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetAvailablePairsUseCase(swapRepository: OldSwapRepository): GetAvailablePairsUseCase {
|
||||
return GetAvailablePairsUseCase(swapRepository = swapRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetSwapSupportedPairsUseCase(
|
||||
|
|
|
|||
|
|
@ -6,9 +6,13 @@ import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
|||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.demo.models.DemoConfig
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
|
||||
import com.tangem.domain.dynamicaddresses.GetDynamicReceiveAddressUseCase
|
||||
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
|
||||
import com.tangem.domain.transaction.GaslessYieldRepository
|
||||
import com.tangem.domain.transaction.usecase.gasless.ResolveGaslessFeePlanUseCase
|
||||
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
|
||||
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
|
||||
import com.tangem.domain.notifications.repository.PushNotificationsRepository
|
||||
|
|
@ -319,30 +323,50 @@ internal object TransactionDomainModule {
|
|||
gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
featureTogglesManager: FeatureTogglesManager,
|
||||
): GetAvailableFeeTokensUseCase {
|
||||
return GetAvailableFeeTokensUseCase(
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
isYieldWithdrawEnabled = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.AND_15632_GASLESS_YIELD_WITHDRAW_ENABLED,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideResolveGaslessFeePlanUseCase(
|
||||
gaslessYieldRepository: GaslessYieldRepository,
|
||||
): ResolveGaslessFeePlanUseCase {
|
||||
return ResolveGaslessFeePlanUseCase(gaslessYieldRepository = gaslessYieldRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetFeeForGaslessUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
gaslessYieldRepository: GaslessYieldRepository,
|
||||
getFeeUseCase: GetFeeUseCase,
|
||||
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
resolveGaslessFeePlanUseCase: ResolveGaslessFeePlanUseCase,
|
||||
featureTogglesManager: FeatureTogglesManager,
|
||||
): GetFeeForGaslessUseCase {
|
||||
return GetFeeForGaslessUseCase(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
demoConfig = DemoConfig,
|
||||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
gaslessYieldRepository = gaslessYieldRepository,
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
getFeeUseCase = getFeeUseCase,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
resolveGaslessFeePlanUseCase = resolveGaslessFeePlanUseCase,
|
||||
isYieldWithdrawEnabled = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.AND_15632_GASLESS_YIELD_WITHDRAW_ENABLED,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -351,15 +375,23 @@ internal object TransactionDomainModule {
|
|||
fun provideGetFeeForTokenUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
gaslessYieldRepository: GaslessYieldRepository,
|
||||
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
resolveGaslessFeePlanUseCase: ResolveGaslessFeePlanUseCase,
|
||||
featureTogglesManager: FeatureTogglesManager,
|
||||
): GetFeeForTokenUseCase {
|
||||
return GetFeeForTokenUseCase(
|
||||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
gaslessYieldRepository = gaslessYieldRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
demoConfig = DemoConfig,
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
resolveGaslessFeePlanUseCase = resolveGaslessFeePlanUseCase,
|
||||
isYieldWithdrawEnabled = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.AND_15632_GASLESS_YIELD_WITHDRAW_ENABLED,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -379,6 +411,7 @@ internal object TransactionDomainModule {
|
|||
singleAccountListSupplier: SingleAccountListSupplier,
|
||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
|
||||
featureTogglesManager: FeatureTogglesManager,
|
||||
): CreateAndSendGaslessTransactionUseCase {
|
||||
return CreateAndSendGaslessTransactionUseCase(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
|
|
@ -386,6 +419,9 @@ internal object TransactionDomainModule {
|
|||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
cardSdkConfigRepository = cardSdkConfigRepository,
|
||||
getHotWalletSigner = tangemHotWalletSignerFactory::create,
|
||||
isGaslessV2Enabled = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.AND_15632_GASLESS_YIELD_WITHDRAW_ENABLED,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -394,15 +430,21 @@ internal object TransactionDomainModule {
|
|||
fun provideEstimateFeeForTokenUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
gaslessYieldRepository: GaslessYieldRepository,
|
||||
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
featureTogglesManager: FeatureTogglesManager,
|
||||
): EstimateFeeForTokenUseCase {
|
||||
return EstimateFeeForTokenUseCase(
|
||||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
gaslessYieldRepository = gaslessYieldRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
demoConfig = DemoConfig,
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
isYieldWithdrawEnabled = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.AND_15632_GASLESS_YIELD_WITHDRAW_ENABLED,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -411,12 +453,14 @@ internal object TransactionDomainModule {
|
|||
fun provideEstimateFeeForGaslessTxUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
gaslessYieldRepository: GaslessYieldRepository,
|
||||
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
estimateFeeUseCase: EstimateFeeUseCase,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
): EstimateFeeForGaslessTxUseCase {
|
||||
return EstimateFeeForGaslessTxUseCase(
|
||||
gaslessTransactionRepository = gaslessTransactionRepository,
|
||||
gaslessYieldRepository = gaslessYieldRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
demoConfig = DemoConfig,
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.tap.di.domain
|
|||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
import com.tangem.domain.common.wallets.UserWalletDataCleaner
|
||||
import com.tangem.domain.common.wallets.UserWalletSelectedHandler
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.transaction.WalletAddressServiceRepository
|
||||
|
|
@ -25,6 +26,7 @@ import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSy
|
|||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase
|
||||
import com.tangem.operations.attestation.CardArtworksProvider
|
||||
import com.tangem.tap.domain.DefaultUserWalletSelectedHandler
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -188,8 +190,16 @@ internal object WalletsDomainModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesDeleteWalletUseCase(userWalletsListRepository: UserWalletsListRepository): DeleteWalletUseCase {
|
||||
return DeleteWalletUseCase(userWalletsListRepository = userWalletsListRepository)
|
||||
fun providesDeleteWalletUseCase(
|
||||
userWalletsListRepository: UserWalletsListRepository,
|
||||
userWalletDataCleaners: Set<@JvmSuppressWildcards UserWalletDataCleaner>,
|
||||
appCoroutineScope: AppCoroutineScope,
|
||||
): DeleteWalletUseCase {
|
||||
return DeleteWalletUseCase(
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
userWalletDataCleaners = userWalletDataCleaners,
|
||||
appCoroutineScope = appCoroutineScope,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent
|
|||
import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
|
||||
import com.tangem.tap.domain.tasks.product.*
|
||||
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
|
||||
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateVirtualAccountAddressTask
|
||||
import com.tangem.tap.domain.tasks.visa.TangemPaySignWithdrawalHashTask
|
||||
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
|
||||
import com.tangem.tap.domain.tasks.visa.VisaCustomerWalletApproveTask
|
||||
|
|
@ -57,8 +58,10 @@ import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask
|
|||
import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask
|
||||
import com.tangem.tap.domain.twins.FinalizeTwinTask
|
||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||
import com.tangem.tap.domain.walletregistration.WalletRegistrationLauncher
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import com.tangem.wallet.R
|
||||
import dagger.Lazy
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
|
@ -72,9 +75,13 @@ internal class DefaultTangemSdkManager(
|
|||
private val visaCardScanHandler: VisaCardScanHandler,
|
||||
private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
|
||||
private val tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
|
||||
private val tangemPayVirtualAccountTaskFactory: TangemPayGenerateVirtualAccountAddressTask.Factory,
|
||||
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
|
||||
private val analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
private val cardRepository: CardRepository,
|
||||
// Lazy breaks a DI cycle: the launcher -> hot wallet accessor -> LegacySettingsRepository ->
|
||||
// TangemSdkManager. It's only needed when a scan actually runs.
|
||||
private val walletRegistrationLauncher: Lazy<WalletRegistrationLauncher>,
|
||||
) : TangemSdkManager {
|
||||
|
||||
private val tangemSdk: TangemSdk
|
||||
|
|
@ -86,7 +93,7 @@ internal class DefaultTangemSdkManager(
|
|||
secureStorage = tangemSdk.secureStorage,
|
||||
)
|
||||
}
|
||||
override val needEnrollBiometrics: Boolean
|
||||
override val isEnrollBiometricsNeeded: Boolean
|
||||
get() {
|
||||
val isNeedEnrollBiometrics = tangemSdk.authenticationManager.needEnrollBiometrics
|
||||
if (isNeedEnrollBiometrics) {
|
||||
|
|
@ -102,7 +109,7 @@ internal class DefaultTangemSdkManager(
|
|||
|
||||
override val canUseBiometry: Boolean
|
||||
get() {
|
||||
val isCanUseBiometry = tangemSdk.authenticationManager.canAuthenticate || needEnrollBiometrics
|
||||
val isCanUseBiometry = tangemSdk.authenticationManager.canAuthenticate || isEnrollBiometricsNeeded
|
||||
if (!isCanUseBiometry) {
|
||||
analyticsErrorHandler.sendErrorEvent(
|
||||
AnalyticsEvent(
|
||||
|
|
@ -124,7 +131,7 @@ internal class DefaultTangemSdkManager(
|
|||
get() = tangemSdk.config.userCodeRequestPolicy
|
||||
|
||||
override suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean): Boolean {
|
||||
return needEnrollBiometrics
|
||||
return isEnrollBiometricsNeeded
|
||||
}
|
||||
|
||||
override suspend fun checkCanUseBiometry(awaitInitialization: Boolean): Boolean {
|
||||
|
|
@ -145,10 +152,11 @@ internal class DefaultTangemSdkManager(
|
|||
card = null,
|
||||
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
|
||||
visaCardScanHandler = visaCardScanHandler,
|
||||
visaCoroutineScope = this,
|
||||
sessionCoroutineScope = this,
|
||||
shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated,
|
||||
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
|
||||
cardRepository = cardRepository,
|
||||
walletRegistrationLauncher = walletRegistrationLauncher.get(),
|
||||
),
|
||||
cardId = cardId,
|
||||
initialMessage = message,
|
||||
|
|
@ -531,6 +539,24 @@ internal class DefaultTangemSdkManager(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun tangemPayProduceVirtualAccountData(
|
||||
preflightReadFilter: PreflightReadFilter,
|
||||
): Either<Throwable, VirtualAccountActivationData> {
|
||||
return coroutineScope {
|
||||
val result = runTaskAsyncReturnOnMain(
|
||||
runnable = tangemPayVirtualAccountTaskFactory.create(coroutineScope = this),
|
||||
cardId = null,
|
||||
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),
|
||||
preflightReadFilter = preflightReadFilter,
|
||||
)
|
||||
|
||||
return@coroutineScope when (result) {
|
||||
is CompletionResult.Failure<*> -> result.error.left()
|
||||
is CompletionResult.Success<VirtualAccountActivationData> -> result.data.right()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getWithdrawalSignature(
|
||||
hash: String,
|
||||
preflightReadFilter: PreflightReadFilter,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import com.tangem.domain.models.scan.ScanResponse
|
|||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.WithdrawalSignatureResult
|
||||
import com.tangem.domain.visa.model.TangemPayInitialCredentials
|
||||
import com.tangem.domain.visa.model.VirtualAccountActivationData
|
||||
import com.tangem.domain.visa.model.VisaActivationInput
|
||||
import com.tangem.domain.visa.model.VisaDataForApprove
|
||||
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
|
||||
|
|
@ -46,7 +47,7 @@ class MockTangemSdkManager(
|
|||
|
||||
override val canUseBiometry: Boolean = false
|
||||
|
||||
override val needEnrollBiometrics: Boolean = false
|
||||
override val isEnrollBiometricsNeeded: Boolean = false
|
||||
|
||||
override val keystoreManager = DummyKeystoreManager()
|
||||
|
||||
|
|
@ -57,7 +58,7 @@ class MockTangemSdkManager(
|
|||
|
||||
override suspend fun checkCanUseBiometry(awaitInitialization: Boolean): Boolean = canUseBiometry
|
||||
|
||||
override suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean): Boolean = needEnrollBiometrics
|
||||
override suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean): Boolean = isEnrollBiometricsNeeded
|
||||
|
||||
override suspend fun scanProduct(
|
||||
cardId: String?,
|
||||
|
|
@ -241,6 +242,12 @@ class MockTangemSdkManager(
|
|||
error("Not implemented")
|
||||
}
|
||||
|
||||
override suspend fun tangemPayProduceVirtualAccountData(
|
||||
preflightReadFilter: PreflightReadFilter,
|
||||
): Either<Throwable, VirtualAccountActivationData> {
|
||||
error("Not implemented")
|
||||
}
|
||||
|
||||
override suspend fun getWithdrawalSignature(
|
||||
hash: String,
|
||||
preflightReadFilter: PreflightReadFilter,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ object MockProvider {
|
|||
MockOption("Shiba (No Backup, No Wallets)") { ShibaNoBackupNoWalletsMockContent },
|
||||
MockOption("Ed25519 Curve") { EdCurveMockContent },
|
||||
MockOption("Secp256k1 Curve") { Secpk1CurveMockContent },
|
||||
MockOption("Wallet 2 (No ed25519_slip0010)") { Wallet2NoEd25519Slip0010MockContent },
|
||||
MockOption("Wallet 1 (Legacy derivation)") { Wallet1LegacyDerivationMockContent },
|
||||
MockOption("Firmware 4.51") { Firmware451MockContent },
|
||||
MockOption("Backup Wallet") { BackupWalletMockContent },
|
||||
MockOption("Dev Wallet") { DevWalletMockContent },
|
||||
MockOption("Firmware 4.12") { Firmware412MockContent },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.tap.domain.sdk.mocks.content
|
||||
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.domain.sdk.mocks.MockContent
|
||||
|
||||
// Firmware 4.51: HD-capable (>= 4.39) but below SolanaTokensAvailable (4.52), so Solana tokens are firmware-limited.
|
||||
object Firmware451MockContent : MockContent by WalletMockContent {
|
||||
|
||||
override val cardDto: CardDTO = WalletMockContent.cardDto.copy(
|
||||
firmwareVersion = WalletMockContent.cardDto.firmwareVersion.copy(minor = 51),
|
||||
)
|
||||
|
||||
override val scanResponse: ScanResponse = WalletMockContent.scanResponse.copy(card = cardDto)
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.tap.domain.sdk.mocks.content
|
||||
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.domain.sdk.mocks.MockContent
|
||||
|
||||
// Wallet1 on a first-batch id (AC01) — resolves to the V1 (legacy) derivation style.
|
||||
object Wallet1LegacyDerivationMockContent : MockContent by WalletMockContent {
|
||||
|
||||
override val cardDto: CardDTO = WalletMockContent.cardDto.copy(batchId = "AC01")
|
||||
|
||||
override val scanResponse: ScanResponse = WalletMockContent.scanResponse.copy(card = cardDto)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.tap.domain.sdk.mocks.content
|
||||
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.domain.sdk.mocks.MockContent
|
||||
|
||||
// Wallet2 without its ed25519_slip0010 wallet, so adding Solana warns UnsupportedCurve (no wallet for its curve).
|
||||
object Wallet2NoEd25519Slip0010MockContent : MockContent by Wallet2WithSeedPhraseMockContent {
|
||||
|
||||
override val cardDto: CardDTO = Wallet2WithSeedPhraseMockContent.cardDto.copy(
|
||||
wallets = Wallet2WithSeedPhraseMockContent.cardDto.wallets.filterNot {
|
||||
it.curve == EllipticCurve.Ed25519Slip0010
|
||||
},
|
||||
)
|
||||
|
||||
override val scanResponse: ScanResponse = Wallet2WithSeedPhraseMockContent.scanResponse.copy(card = cardDto)
|
||||
}
|
||||
|
|
@ -160,6 +160,10 @@ object WalletMockContent : MockContent {
|
|||
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
|
||||
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
|
||||
),
|
||||
DerivationPath("m/44'/111111'/0'/0/0") to ExtendedPublicKey( // Kaspa
|
||||
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
|
||||
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
|
||||
),
|
||||
),
|
||||
extendedPublicKey = ExtendedPublicKey(
|
||||
publicKey = secp256k1WalletPublicKey,
|
||||
|
|
@ -254,6 +258,13 @@ object WalletMockContent : MockContent {
|
|||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||
childNumber = 0,
|
||||
),
|
||||
DerivationPath("m/84'/2'/0'/0/0") to ExtendedPublicKey( // ltc (reuses valid btc key)
|
||||
publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52),
|
||||
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
|
||||
depth = 0,
|
||||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||
childNumber = 0,
|
||||
),
|
||||
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
|
||||
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
|
||||
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
|
||||
|
|
@ -261,6 +272,13 @@ object WalletMockContent : MockContent {
|
|||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||
childNumber = 0,
|
||||
),
|
||||
DerivationPath("m/44'/550'/0'/0/0") to ExtendedPublicKey( // xdc (EVM, reuses valid eth key)
|
||||
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
|
||||
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
|
||||
depth = 0,
|
||||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||
childNumber = 0,
|
||||
),
|
||||
DerivationPath("m/44'/60'/0'/0/1") to ExtendedPublicKey( // eth (account 2)
|
||||
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
|
||||
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
|
||||
|
|
@ -486,6 +504,13 @@ object WalletMockContent : MockContent {
|
|||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||
childNumber = 0,
|
||||
),
|
||||
DerivationPath("m/44'/3030'/0'/0'/0'") to ExtendedPublicKey( // Hedera (address resolves via network)
|
||||
publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109),
|
||||
chainCode = byteArrayOf(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6),
|
||||
depth = 0,
|
||||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||
childNumber = 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,10 @@ import com.tangem.operations.files.ReadFilesTask
|
|||
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
|
||||
import com.tangem.tap.domain.TapSdkError
|
||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||
import com.tangem.tap.domain.walletregistration.WalletRegistrationLauncher
|
||||
import com.tangem.tap.mainScope
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
|
@ -42,10 +45,11 @@ import kotlinx.coroutines.launch
|
|||
internal class ScanProductTask(
|
||||
private val card: Card?,
|
||||
private val visaCardScanHandler: VisaCardScanHandler?,
|
||||
private val visaCoroutineScope: CoroutineScope?,
|
||||
private val sessionCoroutineScope: CoroutineScope?,
|
||||
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?,
|
||||
private val shouldCheckIsAlreadyActivated: Boolean,
|
||||
private val cardRepository: CardRepository,
|
||||
private val walletRegistrationLauncher: WalletRegistrationLauncher? = null,
|
||||
override val allowsRequestAccessCodeFromRepository: Boolean = false,
|
||||
) : CardSessionRunnable<ScanResponse> {
|
||||
|
||||
|
|
@ -97,7 +101,7 @@ internal class ScanProductTask(
|
|||
val processorScanResponseWithNewCard = processorResult.data.copy(
|
||||
card = CardDTO(scanTaskResult.data),
|
||||
)
|
||||
callback(CompletionResult.Success(processorScanResponseWithNewCard))
|
||||
registerColdWalletThenComplete(session, processorScanResponseWithNewCard, callback)
|
||||
}
|
||||
is CompletionResult.Failure -> callback(CompletionResult.Failure(scanTaskResult.error))
|
||||
}
|
||||
|
|
@ -107,6 +111,38 @@ internal class ScanProductTask(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort COLD wallet registration with the Auth Service while the session is still open
|
||||
* (the card is tapped for `AttestWalletKeyTask`); the network POST is deferred by the launcher.
|
||||
* The in-session part (nonce request + attestation) runs before the scan completes, so it can
|
||||
* extend the scan slightly — but it never *fails* the scan: on any error the scan still
|
||||
* completes successfully.
|
||||
*/
|
||||
private fun registerColdWalletThenComplete(
|
||||
session: CardSession,
|
||||
scanResponse: ScanResponse,
|
||||
callback: (result: CompletionResult<ScanResponse>) -> Unit,
|
||||
) {
|
||||
val scope = sessionCoroutineScope
|
||||
val launcher = walletRegistrationLauncher
|
||||
if (scope == null || launcher == null) {
|
||||
TangemLogger.i("Skipping cold wallet registration: coroutine scope or launcher unavailable")
|
||||
callback(CompletionResult.Success(scanResponse))
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
try {
|
||||
runSuspendCatching { launcher.registerColdInSession(session, scanResponse) }
|
||||
.onFailure { TangemLogger.e("Cold wallet registration failed", it) }
|
||||
} finally {
|
||||
// Always complete the scan, even if the registration coroutine is cancelled
|
||||
// (runSuspendCatching rethrows CancellationException) — the scan never depends on
|
||||
// the registration outcome.
|
||||
callback(CompletionResult.Success(scanResponse))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun preflightReadMode(): PreflightReadMode {
|
||||
return if (shouldCheckIsAlreadyActivated) {
|
||||
PreflightReadMode.FullCardReadWithAccessCodeCheck
|
||||
|
|
@ -138,12 +174,12 @@ internal class ScanProductTask(
|
|||
return
|
||||
}
|
||||
|
||||
visaCoroutineScope ?: run {
|
||||
sessionCoroutineScope ?: run {
|
||||
callback(CompletionResult.Failure(TangemSdkError.InsNotSupported()))
|
||||
return
|
||||
}
|
||||
|
||||
visaCoroutineScope.launch {
|
||||
sessionCoroutineScope.launch {
|
||||
when (val result = visaCardScanHandler.handleVisaCardScan(session = session)) {
|
||||
is CompletionResult.Success -> {
|
||||
scanWalletProcessor.proceed(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
package com.tangem.tap.domain.tasks.visa
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.CardWallet
|
||||
import com.tangem.common.core.CardSession
|
||||
import com.tangem.common.core.CardSessionRunnable
|
||||
import com.tangem.common.core.CompletionCallback
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.core.error.ext.tangemError
|
||||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
import com.tangem.domain.card.common.visa.VisaUtilities
|
||||
import com.tangem.domain.visa.error.VisaActivationError
|
||||
import com.tangem.domain.visa.model.VirtualAccountActivationData
|
||||
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Derives the Virtual Account key ([VisaUtilities.virtualAccountDerivationPath]) on the card and
|
||||
* generates its deposit address. The derived key is returned (keyed by the seed wallet public key)
|
||||
* so the caller can persist it via `DerivationsRepository.storeDerivedKeys` — no second tap needed.
|
||||
*/
|
||||
class TangemPayGenerateVirtualAccountAddressTask @AssistedInject constructor(
|
||||
@Assisted private val coroutineScope: CoroutineScope,
|
||||
) : CardSessionRunnable<VirtualAccountActivationData> {
|
||||
|
||||
override fun run(session: CardSession, callback: CompletionCallback<VirtualAccountActivationData>) {
|
||||
coroutineScope.launch {
|
||||
callback(runSuspend(session = session))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun runSuspend(session: CardSession): CompletionResult<VirtualAccountActivationData> {
|
||||
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.curve }
|
||||
?: return CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError)
|
||||
|
||||
val extendedPublicKey = when (val derivationResult = runDerivationTask(session, wallet)) {
|
||||
is CompletionResult.Failure<*> -> return CompletionResult.Failure(derivationResult.error)
|
||||
is CompletionResult.Success<ExtendedPublicKey> -> derivationResult.data
|
||||
}
|
||||
|
||||
val address = VisaUtilities.generateAddressFromExtendedKey(extendedPublicKey = extendedPublicKey)
|
||||
|
||||
val derivedKeys = mapOf(
|
||||
wallet.publicKey.toMapKey() to ExtendedPublicKeysMap(
|
||||
mapOf(VisaUtilities.virtualAccountDerivationPath to extendedPublicKey),
|
||||
),
|
||||
)
|
||||
|
||||
return CompletionResult.Success(
|
||||
data = VirtualAccountActivationData(address = address, derivedKeys = derivedKeys),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun runDerivationTask(
|
||||
session: CardSession,
|
||||
wallet: CardWallet,
|
||||
): CompletionResult<ExtendedPublicKey> {
|
||||
val deferred = CompletableDeferred<CompletionResult<ExtendedPublicKey>>()
|
||||
val derivationTask = DeriveWalletPublicKeyTask(
|
||||
walletPublicKey = wallet.publicKey,
|
||||
derivationPath = VisaUtilities.virtualAccountDerivationPath,
|
||||
)
|
||||
|
||||
derivationTask.run(session = session, callback = deferred::complete)
|
||||
return deferred.await()
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(coroutineScope: CoroutineScope): TangemPayGenerateVirtualAccountAddressTask
|
||||
}
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ class FinalizeTwinTask(
|
|||
ScanProductTask(
|
||||
card = readResult.data,
|
||||
visaCardScanHandler = null,
|
||||
visaCoroutineScope = null,
|
||||
sessionCoroutineScope = null,
|
||||
shouldCheckIsAlreadyActivated = false,
|
||||
onboardingV2FeatureToggles = null,
|
||||
cardRepository = cardRepository,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.tap.domain.userWalletList.di
|
||||
|
||||
import com.tangem.domain.common.wallets.UserWalletDataCleaner
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.Multibinds
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface UserWalletDataCleanerModule {
|
||||
|
||||
@Multibinds
|
||||
fun userWalletDataCleaners(): Set<UserWalletDataCleaner>
|
||||
}
|
||||
|
|
@ -14,7 +14,6 @@ import com.tangem.core.analytics.utils.TrackingContextProxy
|
|||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
||||
import com.tangem.domain.appsflyer.AppsFlyerDeeplinkSource
|
||||
import com.tangem.domain.appsflyer.usecase.ClearAppsFlyerDeeplinkUseCase
|
||||
import com.tangem.domain.common.wallets.UserWalletSelectedHandler
|
||||
import com.tangem.domain.common.wallets.UserWalletTransformAction
|
||||
|
|
@ -622,11 +621,12 @@ internal class DefaultUserWalletsListRepository(
|
|||
|
||||
private suspend fun onFirstWalletCreated() {
|
||||
// reset the referral attribution (set from AF deeplink) after creating a new wallet
|
||||
clearAppsFlyerDeeplinkUseCase(AppsFlyerDeeplinkSource.Referral)
|
||||
clearAppsFlyerDeeplinkUseCase()
|
||||
}
|
||||
|
||||
private suspend fun onAllWalletsDeleted() {
|
||||
// reset the referral attribution (set from AF deeplink) after removing the last wallet
|
||||
clearAppsFlyerDeeplinkUseCase(AppsFlyerDeeplinkSource.Referral)
|
||||
clearAppsFlyerDeeplinkUseCase()
|
||||
appPreferencesStore.editData { it.remove(PreferencesKeys.USEDESK_CLIENT_ID_KEY) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
package com.tangem.tap.domain.walletregistration
|
||||
|
||||
import com.tangem.blockchain.common.UnmarshalHelper
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.core.CardSession
|
||||
import com.tangem.common.extensions.calculateSha256
|
||||
import com.tangem.common.extensions.toDecompressedPublicKey
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.lib.auth.session.WalletSignatureBundle
|
||||
import com.tangem.lib.auth.session.WalletSigner
|
||||
import com.tangem.operations.attestation.AttestWalletKeyResponse
|
||||
import com.tangem.operations.attestation.AttestWalletKeyTask
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
/**
|
||||
* Builds a [WalletSigner] for a COLD (card-backed) wallet. Runs `AttestWalletKeyTask` in Dynamic
|
||||
* mode **inside an already-open [CardSession]** (no extra tap): the card produces the wallet
|
||||
* signature and, on COS 2.01+, the card signature over the wallet nonce. On older cards without a
|
||||
* card signature the wallet is registered with the wallet signature only (backend treats it as hot).
|
||||
*/
|
||||
internal class ColdWalletRegistrationSigner @Inject constructor() {
|
||||
|
||||
fun signerFor(session: CardSession, scanResponse: ScanResponse): WalletSigner = WalletSigner { nonceBytes ->
|
||||
val card = scanResponse.card
|
||||
val walletPublicKey = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }?.publicKey
|
||||
?: error("No secp256k1 wallet on card ${card.cardId}")
|
||||
|
||||
buildBundle(
|
||||
response = attest(session, walletPublicKey, nonceBytes),
|
||||
walletPublicKey = walletPublicKey,
|
||||
cardPublicKey = card.cardPublicKey,
|
||||
nonceBytes = nonceBytes,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure mapping of an [AttestWalletKeyResponse] to a [WalletSignatureBundle] (no card session) —
|
||||
* unit-testable in isolation.
|
||||
*/
|
||||
fun buildBundle(
|
||||
response: AttestWalletKeyResponse,
|
||||
walletPublicKey: ByteArray,
|
||||
cardPublicKey: ByteArray,
|
||||
nonceBytes: ByteArray,
|
||||
): WalletSignatureBundle {
|
||||
val salt = response.salt
|
||||
val walletSignatureRsv = UnmarshalHelper.unmarshalSignatureExtended(
|
||||
signature = response.walletSignature,
|
||||
hash = (nonceBytes + salt).calculateSha256(),
|
||||
publicKey = walletPublicKey.toDecompressedPublicKey(),
|
||||
).asRSVLegacyEVM()
|
||||
|
||||
val cardSignature = response.cardSignature
|
||||
val publicKeySalt = response.publicKeySalt
|
||||
// The dynamic card signature (proving the card owns the wallet) requires COS 2.01+. On older
|
||||
// cards it is absent — we then register with the wallet signature only, and the backend
|
||||
// treats such a wallet as hot (no card-ownership proof).
|
||||
if (cardSignature == null || publicKeySalt == null) {
|
||||
return WalletSignatureBundle(
|
||||
walletSignature = walletSignatureRsv,
|
||||
walletSignatureSalt = salt,
|
||||
cardSignature = null,
|
||||
cardSignatureSalt = null,
|
||||
walletStatusByte = null,
|
||||
)
|
||||
}
|
||||
|
||||
val walletStatusByte = response.walletStatus?.code?.toByte()
|
||||
// Card-signature preimage: walletPublicKey | challenge | publicKeySalt [| walletStatus].
|
||||
// The walletStatus byte is appended only when the card reports it (COS 6+).
|
||||
val cardMessage = walletPublicKey + nonceBytes + publicKeySalt +
|
||||
(walletStatusByte?.let { byteArrayOf(it) } ?: ByteArray(size = 0))
|
||||
val cardSignatureRsv = UnmarshalHelper.unmarshalSignatureExtended(
|
||||
signature = cardSignature,
|
||||
hash = cardMessage.calculateSha256(),
|
||||
publicKey = cardPublicKey.toDecompressedPublicKey(),
|
||||
).asRSVLegacyEVM()
|
||||
|
||||
return WalletSignatureBundle(
|
||||
walletSignature = walletSignatureRsv,
|
||||
walletSignatureSalt = salt,
|
||||
cardSignature = cardSignatureRsv,
|
||||
cardSignatureSalt = publicKeySalt,
|
||||
walletStatusByte = walletStatusByte,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun attest(
|
||||
session: CardSession,
|
||||
walletPublicKey: ByteArray,
|
||||
nonceBytes: ByteArray,
|
||||
): AttestWalletKeyResponse {
|
||||
val result = suspendCancellableCoroutine { continuation ->
|
||||
AttestWalletKeyTask(publicKey = walletPublicKey, challenge = nonceBytes)
|
||||
.run(session) { if (continuation.isActive) continuation.resume(it) }
|
||||
}
|
||||
return when (result) {
|
||||
is CompletionResult.Success -> result.data
|
||||
is CompletionResult.Failure -> throw ColdWalletAttestationException(result.error.customMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** `AttestWalletKeyTask` failed (NFC error, verification failure, user cancelled). */
|
||||
internal class ColdWalletAttestationException(message: String) :
|
||||
Exception("Cold wallet attestation failed: $message")
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.tap.domain.walletregistration
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.registration.WalletRegistrationTrigger
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultWalletRegistrationTrigger @Inject constructor(
|
||||
private val launcher: WalletRegistrationLauncher,
|
||||
) : WalletRegistrationTrigger {
|
||||
|
||||
override suspend fun onMobileWalletCreated(userWallet: UserWallet.Hot) {
|
||||
launcher.registerMobile(userWallet)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package com.tangem.tap.domain.walletregistration
|
||||
|
||||
import com.tangem.blockchain.common.UnmarshalHelper
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.extensions.calculateSha256
|
||||
import com.tangem.common.extensions.toDecompressedPublicKey
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessor
|
||||
import com.tangem.hot.sdk.model.DataToSign
|
||||
import com.tangem.lib.auth.session.WalletSignatureBundle
|
||||
import com.tangem.lib.auth.session.WalletSigner
|
||||
import java.security.SecureRandom
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Builds a [WalletSigner] for a MOBILE (hot/software) wallet. The wallet's secp256k1 key signs
|
||||
* `sha256(nonceBytes || walletSignatureSalt)` via the hot wallet SDK; there is no card, so the
|
||||
* card-signature fields stay null.
|
||||
*/
|
||||
internal class MobileWalletRegistrationSigner @Inject constructor(
|
||||
private val hotWalletAccessor: HotWalletAccessor,
|
||||
) {
|
||||
|
||||
fun signerFor(userWallet: UserWallet.Hot): WalletSigner = WalletSigner { nonceBytes ->
|
||||
val wallet = userWallet.wallets
|
||||
?.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
|
||||
?: error("No secp256k1 wallet available for hot wallet ${userWallet.walletId}")
|
||||
|
||||
val salt = ByteArray(SALT_SIZE).also(secureRandom::nextBytes)
|
||||
val hash = (nonceBytes + salt).calculateSha256()
|
||||
|
||||
val signature = hotWalletAccessor.signHashes(
|
||||
hotWalletId = userWallet.hotWalletId,
|
||||
dataToSign = listOf(DataToSign(curve = EllipticCurve.Secp256k1, hashes = listOf(hash))),
|
||||
).first().signatures.first()
|
||||
|
||||
val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended(
|
||||
signature = signature,
|
||||
hash = hash,
|
||||
publicKey = wallet.publicKey.toDecompressedPublicKey(),
|
||||
).asRSVLegacyEVM()
|
||||
|
||||
WalletSignatureBundle(
|
||||
walletSignature = rsvSignature,
|
||||
walletSignatureSalt = salt,
|
||||
cardSignature = null,
|
||||
cardSignatureSalt = null,
|
||||
walletStatusByte = null,
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val SALT_SIZE = 16
|
||||
val secureRandom = SecureRandom()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package com.tangem.tap.domain.walletregistration
|
||||
|
||||
import android.util.Base64
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.core.CardSession
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.lib.auth.AuthFeatureToggles
|
||||
import com.tangem.lib.auth.session.WalletRegistrar
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Single entry point that registers wallets with the Tangem Auth Service. Gated by the backend-auth
|
||||
* feature toggle; all failures are log-only (registration is retried on the next launch / card scan,
|
||||
* never blocks the user). MOBILE wallets register without UI; COLD wallets attest inside a live
|
||||
* card session (no extra tap) and POST after the session closes.
|
||||
*/
|
||||
internal class WalletRegistrationLauncher @Inject constructor(
|
||||
private val walletRegistrar: WalletRegistrar,
|
||||
private val mobileSigner: MobileWalletRegistrationSigner,
|
||||
private val coldSigner: ColdWalletRegistrationSigner,
|
||||
private val authFeatureToggles: AuthFeatureToggles,
|
||||
private val appCoroutineScope: AppCoroutineScope,
|
||||
) {
|
||||
|
||||
/**
|
||||
|
||||
* Never throws (beyond cooperative cancellation) — any unexpected failure is caught and logged,
|
||||
* so callers relying on the fire-and-forget contract stay safe.
|
||||
*/
|
||||
suspend fun registerMobile(userWallet: UserWallet.Hot) {
|
||||
if (!authFeatureToggles.isBackendAuthenticationEnabled) return
|
||||
|
||||
runSuspendCatching {
|
||||
walletRegistrar.register(
|
||||
walletId = userWallet.walletId.toBase64(),
|
||||
signer = mobileSigner.signerFor(userWallet),
|
||||
).onLeft { TangemLogger.e("Mobile wallet registration deferred: $it") }
|
||||
}.onFailure { TangemLogger.e("Mobile wallet registration failed", it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* COLD registration. Phase 1 ([WalletRegistrar.prepare]) runs inside the still-open [session]
|
||||
* (the card is tapped here); phase 2 (the network POST) is dispatched on [appCoroutineScope]
|
||||
* after this returns, so the user doesn't hold the card during the request. Call this BEFORE
|
||||
* the scan completes its session callback.
|
||||
*/
|
||||
suspend fun registerColdInSession(session: CardSession, scanResponse: ScanResponse) {
|
||||
if (!authFeatureToggles.isBackendAuthenticationEnabled) return
|
||||
|
||||
val walletId = UserWalletIdBuilder.scanResponse(scanResponse).build()?.toBase64() ?: return
|
||||
|
||||
val prepared = walletRegistrar.prepare(walletId, coldSigner.signerFor(session, scanResponse))
|
||||
.getOrElse { error ->
|
||||
TangemLogger.e("Cold wallet registration prepare deferred: $error")
|
||||
return
|
||||
}
|
||||
if (prepared == null) return // already registered
|
||||
|
||||
appCoroutineScope.launch {
|
||||
runSuspendCatching {
|
||||
walletRegistrar.submit(prepared)
|
||||
.onLeft { TangemLogger.e("Cold wallet registration submit deferred: $it") }
|
||||
}.onFailure { TangemLogger.e("Cold wallet registration submit failed", it) }
|
||||
}
|
||||
}
|
||||
|
||||
/** Launch-time safety net: registers any not-yet-registered MOBILE wallets (no UI). */
|
||||
suspend fun retryMobileRegistrations(userWallets: List<UserWallet>) {
|
||||
if (!authFeatureToggles.isBackendAuthenticationEnabled) return
|
||||
|
||||
userWallets.filterIsInstance<UserWallet.Hot>().forEach { registerMobile(it) }
|
||||
}
|
||||
|
||||
private fun UserWalletId.toBase64(): String = Base64.encodeToString(value, Base64.NO_WRAP)
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.tap.domain.walletregistration
|
||||
|
||||
import com.tangem.domain.wallets.registration.WalletRegistrationTrigger
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface WalletRegistrationModule {
|
||||
|
||||
@Binds
|
||||
fun bindWalletRegistrationTrigger(impl: DefaultWalletRegistrationTrigger): WalletRegistrationTrigger
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ internal fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () ->
|
|||
modifier = modifier,
|
||||
titleRes = R.string.app_settings_title,
|
||||
addBottomInsets = false,
|
||||
backButtonTestTag = AppSettingsScreenTestTags.BACK_BUTTON,
|
||||
content = {
|
||||
when (state) {
|
||||
is AppSettingsScreenState.Content -> AppSettings(state = state)
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ internal class AppSettingsModel @Inject constructor(
|
|||
private fun observeBiometricsStatusChanges() {
|
||||
flow {
|
||||
do {
|
||||
val isEnrollBiometricsNeeded = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull()
|
||||
val isEnrollBiometricsNeeded = runCatching(tangemSdkManager::isEnrollBiometricsNeeded).getOrNull()
|
||||
if (isEnrollBiometricsNeeded != null) {
|
||||
emit(isEnrollBiometricsNeeded)
|
||||
}
|
||||
|
|
@ -366,7 +366,7 @@ internal class AppSettingsModel @Inject constructor(
|
|||
localState.update { state ->
|
||||
state.copy(
|
||||
hasSecuredWallets = userWalletsListRepository.hasSecuredWallets(),
|
||||
isEnrollBiometricsNeeded = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() == true,
|
||||
isEnrollBiometricsNeeded = runCatching(tangemSdkManager::isEnrollBiometricsNeeded).getOrNull() == true,
|
||||
isBiometricAuthenticationUsed = walletsRepository.useBiometricAuthentication(),
|
||||
isAccessCodeRequired = walletsRepository.requireAccessCode(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconEnd
|
||||
|
|
@ -23,6 +24,7 @@ internal fun SettingsScreensScaffold(
|
|||
@StringRes titleRes: Int? = null,
|
||||
addBottomInsets: Boolean = true,
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
backButtonTestTag: String? = null,
|
||||
content: @Composable () -> Unit,
|
||||
fab: @Composable () -> Unit = {},
|
||||
) {
|
||||
|
|
@ -35,6 +37,7 @@ internal fun SettingsScreensScaffold(
|
|||
modifier = Modifier.statusBarsPadding(),
|
||||
onBackClick = onBackClick,
|
||||
backgroundColor = backgroundColor,
|
||||
backButtonTestTag = backButtonTestTag,
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
|
|
@ -91,13 +94,17 @@ internal fun EmptyTopBarWithNavigation(
|
|||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundColor: Color = TangemTheme.colors.background.primary,
|
||||
backButtonTestTag: String? = null,
|
||||
) {
|
||||
TopAppBar(
|
||||
modifier = modifier,
|
||||
title = { },
|
||||
navigationIcon =
|
||||
{
|
||||
IconButton(onClick = onBackClick) {
|
||||
IconButton(
|
||||
onClick = onBackClick,
|
||||
modifier = if (backButtonTestTag != null) Modifier.testTag(backButtonTestTag) else Modifier,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_back_24),
|
||||
contentDescription = null,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.tap.features.root
|
||||
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class DefaultRootWarningContinuation @Inject constructor() : RootWarningContinuation {
|
||||
|
||||
private val dismissals = Channel<Unit>(capacity = Channel.CONFLATED)
|
||||
|
||||
override suspend fun awaitDismiss() {
|
||||
dismissals.receive()
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
dismissals.trySend(Unit)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +1,40 @@
|
|||
package com.tangem.tap.features.root
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.essenty.instancekeeper.getOrCreateSimple
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.components.DialogFullScreen
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.security.DeviceSecurityInfoProvider
|
||||
import com.tangem.security.isSecurityExposed
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Full-screen root-detected security warning. Presentational only — its visibility is controlled by the
|
||||
* startup gate (via a childSlot); "Continue" resolves [RootWarningContinuation]. Whether it should be shown
|
||||
* at all (and marking it as shown) is decided by the gate.
|
||||
*/
|
||||
@Suppress("UnusedPrivateProperty")
|
||||
class RootDetectedWarningComponent @AssistedInject constructor(
|
||||
internal class RootDetectedWarningComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: Unit,
|
||||
private val securityInfoProvider: DeviceSecurityInfoProvider,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val rootWarningContinuation: RootWarningContinuation,
|
||||
) : AppComponentContext by appComponentContext, ComposableContentComponent {
|
||||
|
||||
private val isShown = instanceKeeper.getOrCreateSimple { MutableStateFlow(false) }
|
||||
|
||||
suspend fun shouldShowWarning(): Boolean {
|
||||
return settingsRepository.isRootDetectedWarningShown().not() && securityInfoProvider.isSecurityExposed()
|
||||
}
|
||||
|
||||
suspend fun tryToShowWarningAndWaitContinuation() {
|
||||
if (isShown.value) return
|
||||
|
||||
if (shouldShowWarning()) {
|
||||
isShown.value = true
|
||||
}
|
||||
|
||||
isShown.first { it == false } // Wait until the warning is dismissed
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val isShownState by isShown.collectAsStateWithLifecycle()
|
||||
|
||||
if (isShownState) {
|
||||
DialogFullScreen(onDismissRequest = {}) {
|
||||
RootDetectedWarningContent(
|
||||
modifier = modifier,
|
||||
onContinueClick = remember(this) { ::onContinueClick },
|
||||
)
|
||||
}
|
||||
DialogFullScreen(onDismissRequest = {}) {
|
||||
RootDetectedWarningContent(
|
||||
modifier = modifier,
|
||||
onContinueClick = remember(this) { ::onContinueClick },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onContinueClick() {
|
||||
componentScope.launch {
|
||||
settingsRepository.setRootDetectedWarningShown(true)
|
||||
isShown.value = false
|
||||
}
|
||||
rootWarningContinuation.dismiss()
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.tap.features.root
|
||||
|
||||
/**
|
||||
* Resumes the startup gate after the root-detected security warning is dismissed.
|
||||
*
|
||||
* The gate awaits [awaitDismiss] while the warning is shown, and the screen calls [dismiss] on "Continue".
|
||||
*/
|
||||
interface RootWarningContinuation {
|
||||
|
||||
suspend fun awaitDismiss()
|
||||
|
||||
fun dismiss()
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.tap.features.root.di
|
||||
|
||||
import com.tangem.tap.features.root.DefaultRootWarningContinuation
|
||||
import com.tangem.tap.features.root.RootWarningContinuation
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface RootModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindRootWarningContinuation(impl: DefaultRootWarningContinuation): RootWarningContinuation
|
||||
}
|
||||
|
|
@ -42,8 +42,9 @@ internal fun RootContent(
|
|||
onBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
wcContent: @Composable (modifier: Modifier) -> Unit,
|
||||
promoContent: @Composable (modifier: Modifier) -> Unit,
|
||||
hotAccessCodeContent: @Composable (modifier: Modifier) -> Unit,
|
||||
rootDetectedWarningContent: @Composable (modifier: Modifier) -> Unit,
|
||||
startupGateContent: @Composable (modifier: Modifier) -> Unit,
|
||||
scanFailsContent: @Composable (modifier: Modifier) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
|
@ -80,9 +81,11 @@ internal fun RootContent(
|
|||
|
||||
wcContent(Modifier.fillMaxSize())
|
||||
|
||||
promoContent(Modifier.fillMaxSize())
|
||||
|
||||
hotAccessCodeContent(Modifier.fillMaxSize())
|
||||
|
||||
rootDetectedWarningContent(Modifier.fillMaxSize())
|
||||
startupGateContent(Modifier.fillMaxSize())
|
||||
|
||||
scanFailsContent(Modifier.fillMaxSize())
|
||||
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ import com.tangem.core.analytics.models.Basic
|
|||
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
||||
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
|
|
@ -31,7 +29,6 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerDeeplinkSource
|
||||
import com.tangem.domain.card.repository.CardRepository
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
|
|
@ -47,20 +44,21 @@ import com.tangem.features.hotwallet.HotAccessCodeRequestComponent
|
|||
import com.tangem.features.hotwallet.accesscoderequest.proxy.HotWalletPasswordRequesterProxy
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
|
||||
import com.tangem.features.promobanners.api.swapcashback.CampaignsComponent
|
||||
import com.tangem.features.walletconnect.components.WcRoutingComponent
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.hot.sdk.android.create
|
||||
import com.tangem.sdk.api.BackupServiceHolder
|
||||
import com.tangem.tap.common.SnackbarHandler
|
||||
import com.tangem.tap.common.analytics.appsflyer.AppsFlyerReferralParamsHandler
|
||||
import com.tangem.tap.common.analytics.appsflyer.AppsFlyerDeeplinkRouter
|
||||
import com.tangem.tap.features.hot.TangemHotSDKProxy
|
||||
import com.tangem.tap.features.root.RootDetectedWarningComponent
|
||||
import com.tangem.tap.features.scanfails.ScanFailsComponent
|
||||
import com.tangem.tap.features.scanfails.ScanFailsRequesterProxy
|
||||
import com.tangem.tap.routing.RootContent
|
||||
import com.tangem.tap.routing.component.RoutingComponent
|
||||
import com.tangem.tap.routing.component.RoutingComponent.Child
|
||||
import com.tangem.tap.routing.configurator.AppRouterConfig
|
||||
import com.tangem.tap.routing.startup.AppStartupGateComponent
|
||||
import com.tangem.tap.routing.utils.ChildFactory
|
||||
import com.tangem.tap.routing.utils.DeepLinkFactory
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
|
@ -68,11 +66,8 @@ import com.tangem.wallet.R
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
internal class DefaultRoutingComponent @AssistedInject constructor(
|
||||
|
|
@ -83,15 +78,16 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
private val appRouterConfig: AppRouterConfig,
|
||||
private val uiDependencies: UiDependencies,
|
||||
private val wcRoutingComponentFactory: WcRoutingComponent.Factory,
|
||||
private val campaignsComponentFactory: CampaignsComponent.Factory,
|
||||
private val deeplinkFactory: DeepLinkFactory,
|
||||
private val tangemHotSDKProxy: TangemHotSDKProxy,
|
||||
private val hotAccessCodeRequestComponentFactory: HotAccessCodeRequestComponent.Factory,
|
||||
private val hotAccessCodeRequesterProxy: HotWalletPasswordRequesterProxy,
|
||||
private val rootDetectedWarningComponentFactory: RootDetectedWarningComponent.Factory,
|
||||
private val appStartupGateComponentFactory: AppStartupGateComponent.Factory,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val cardRepository: CardRepository,
|
||||
private val onboardingRepository: OnboardingRepository,
|
||||
private val appsFlyerReferralParamsHandler: AppsFlyerReferralParamsHandler,
|
||||
private val appsFlyerDeeplinkRouter: AppsFlyerDeeplinkRouter,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
private val scanFailsComponentFactory: ScanFailsComponent.Factory,
|
||||
private val scanFailsRequesterProxy: ScanFailsRequesterProxy,
|
||||
|
|
@ -102,7 +98,6 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
private val neverToInitiallyAskPermissionUseCase: NeverToInitiallyAskPermissionUseCase,
|
||||
private val shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase,
|
||||
private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase,
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : RoutingComponent,
|
||||
AppComponentContext by context,
|
||||
SnackbarHandler {
|
||||
|
|
@ -112,14 +107,18 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
.create(child("wcRoutingComponent"), params = Unit)
|
||||
}
|
||||
|
||||
private val campaignsComponent: CampaignsComponent by lazy {
|
||||
campaignsComponentFactory
|
||||
.create(child("swapCashbackCampaign"), params = Unit)
|
||||
}
|
||||
|
||||
private val hotAccessCodeRequestComponent: HotAccessCodeRequestComponent by lazy {
|
||||
hotAccessCodeRequestComponentFactory
|
||||
.create(child("hotAccessCodeRequestComponent"), Unit)
|
||||
}
|
||||
|
||||
private val rootDetectedWarningComponent: RootDetectedWarningComponent by lazy {
|
||||
rootDetectedWarningComponentFactory
|
||||
.create(child("rootDetectedWarningComponent"), Unit)
|
||||
private val appStartupGateComponent: AppStartupGateComponent by lazy {
|
||||
appStartupGateComponentFactory.create(child("appStartupGate"))
|
||||
}
|
||||
|
||||
private val scanFailsComponent: ScanFailsComponent by lazy {
|
||||
|
|
@ -147,6 +146,8 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
},
|
||||
)
|
||||
|
||||
private val currentRoute = MutableStateFlow<AppRoute?>(null)
|
||||
|
||||
init {
|
||||
appRouterConfig.routerScope = componentScope
|
||||
appRouterConfig.componentRouter = router
|
||||
|
|
@ -161,6 +162,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
|
||||
val stackItems = stack.items.map { it.configuration }
|
||||
|
||||
currentRoute.value = stack.active.configuration
|
||||
wcRoutingComponent.onAppRouteChange(stack.active.configuration)
|
||||
deeplinkFactory.checkRoutingReadiness(stack.active.configuration)
|
||||
|
||||
|
|
@ -171,47 +173,46 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
|
||||
configureProxies()
|
||||
initializeInitialNavigation()
|
||||
appsFlyerDeeplinkRouter.observe(scope = componentScope, currentRoute = currentRoute)
|
||||
}
|
||||
|
||||
private fun initializeInitialNavigation() {
|
||||
if (initialStack.isNullOrEmpty()) {
|
||||
componentScope.launch {
|
||||
val initialRoute = resolveInitialRoute()
|
||||
if (rootDetectedWarningComponent.shouldShowWarning()) {
|
||||
launch(dispatchers.main) {
|
||||
rootDetectedWarningComponent.tryToShowWarningAndWaitContinuation()
|
||||
router.replaceAll(initialRoute)
|
||||
}
|
||||
} else {
|
||||
router.replaceAll(initialRoute)
|
||||
}
|
||||
}
|
||||
componentScope.launch { resolveAndNavigate() }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun resolveInitialRoute(): AppRoute {
|
||||
private suspend fun resolveAndNavigate() {
|
||||
appStartupGateComponent.await()
|
||||
navigateToStartRoute()
|
||||
}
|
||||
|
||||
private suspend fun navigateToStartRoute() {
|
||||
val initialRoute = resolveStartRoute()
|
||||
onInitialRouteResolved(initialRoute)
|
||||
router.replaceAll(initialRoute)
|
||||
}
|
||||
|
||||
private suspend fun resolveStartRoute(): AppRoute {
|
||||
val userWallets = userWalletsListRepository.userWalletsSync()
|
||||
|
||||
return when {
|
||||
userWallets.isEmpty() -> navigateForEmptyWallets()
|
||||
userWallets.any { it.isLocked } -> {
|
||||
AppRoute.Welcome(
|
||||
launchMode = launchMode,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
trackSignInEvent()
|
||||
AppRoute.Wallet
|
||||
}
|
||||
}.also {
|
||||
appRouterConfig.initializedState.value = true
|
||||
checkForUnfinishedBackup()
|
||||
userWallets.any { it.isLocked } -> AppRoute.Welcome(launchMode = launchMode)
|
||||
else -> AppRoute.Wallet
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onInitialRouteResolved(route: AppRoute) {
|
||||
appRouterConfig.initializedState.value = true
|
||||
if (route is AppRoute.Wallet) trackSignInEvent()
|
||||
checkForUnfinishedBackup()
|
||||
}
|
||||
|
||||
private suspend fun navigateForEmptyWallets(): AppRoute {
|
||||
val afterEmptyRoute = resolveAppsFlyerOnboardingRoute()
|
||||
?: AppRoute.Home(launchMode = launchMode)
|
||||
// AppsFlyer deep links (referral, tpay_mobileonboard) are routed reactively by AppsFlyerDeeplinkRouter
|
||||
// once we settle on an idle screen — not resolved here.
|
||||
val afterEmptyRoute = AppRoute.Home(launchMode = launchMode)
|
||||
|
||||
val shouldAskPushPermission = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrNull()
|
||||
?: return afterEmptyRoute
|
||||
|
|
@ -231,44 +232,6 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun resolveAppsFlyerOnboardingRoute(): AppRoute? = coroutineScope {
|
||||
val tangemPayRoute = async { resolveTangemPayHotWalletOnboardingRoute() }
|
||||
val referralRoute = async { resolveReferralRoute() }
|
||||
tangemPayRoute.await() ?: referralRoute.await()
|
||||
}
|
||||
|
||||
private suspend fun resolveTangemPayHotWalletOnboardingRoute(): AppRoute? {
|
||||
val isEnabled = featureTogglesManager.isFeatureEnabled(
|
||||
FeatureToggles.AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING,
|
||||
)
|
||||
TangemLogger.i("[TangemPay][HWO] Feature toggle enabled=$isEnabled")
|
||||
if (!isEnabled) return null
|
||||
|
||||
val deepLink = awaitAppsFlyerDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding)
|
||||
TangemLogger.i("[TangemPay][HWO] Deep link present=${deepLink != null}")
|
||||
return if (deepLink != null) AppRoute.TangemPayHotWalletOnboarding else null
|
||||
}
|
||||
|
||||
private suspend fun resolveReferralRoute(): AppRoute? {
|
||||
val isEnabled = featureTogglesManager.isFeatureEnabled(
|
||||
FeatureToggles.TWI_1512_HIDE_STORIES_FOR_REFERRAL_ENABLED,
|
||||
)
|
||||
if (!isEnabled) return null
|
||||
|
||||
val referralDeepLink = awaitAppsFlyerDeeplink(AppsFlyerDeeplinkSource.Referral)
|
||||
return if (referralDeepLink != null) {
|
||||
AppRoute.CreateWalletStart(mode = AppRoute.CreateWalletStart.Mode.HotWallet)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun awaitAppsFlyerDeeplink(source: AppsFlyerDeeplinkSource): String? {
|
||||
return withTimeoutOrNull(2.seconds) {
|
||||
appsFlyerReferralParamsHandler.waitForDeeplink(source)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
RootContent(
|
||||
|
|
@ -278,8 +241,9 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
onBack = router::pop,
|
||||
modifier = modifier,
|
||||
wcContent = { wcRoutingComponent.Content(it) },
|
||||
promoContent = { campaignsComponent.Content(it) },
|
||||
hotAccessCodeContent = { hotAccessCodeRequestComponent.Content(it) },
|
||||
rootDetectedWarningContent = { rootDetectedWarningComponent.Content(it) },
|
||||
startupGateContent = { appStartupGateComponent.Content(it) },
|
||||
scanFailsContent = { scanFailsComponent.Content(it) },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
package com.tangem.tap.routing.startup
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.ChildSlot
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.arkivanov.decompose.value.Value
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.appupdate.model.AppUpdateState
|
||||
import com.tangem.domain.appupdate.usecase.GetAppUpdateStateUseCase
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.features.forceupdate.ForceUpdateComponent
|
||||
import com.tangem.features.forceupdate.ForceUpdateContinuation
|
||||
import com.tangem.features.forceupdate.ForceUpdateFeatureToggles
|
||||
import com.tangem.security.DeviceSecurityInfoProvider
|
||||
import com.tangem.security.isSecurityExposed
|
||||
import com.tangem.tap.features.root.RootDetectedWarningComponent
|
||||
import com.tangem.tap.features.root.RootWarningContinuation
|
||||
import com.tangem.tap.routing.configurator.AppRouterConfig
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Owns the pre-start gates shown before the regular startup navigation — the force-update screen and the
|
||||
* root-detected security warning — as interchangeable full-screen overlays in a single [childSlot].
|
||||
* [await] runs them in order and returns when the app may proceed, so the routing component stays agnostic.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
internal class AppStartupGateComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
private val getAppUpdateStateUseCase: GetAppUpdateStateUseCase,
|
||||
private val forceUpdateFeatureToggles: ForceUpdateFeatureToggles,
|
||||
private val forceUpdateContinuation: ForceUpdateContinuation,
|
||||
private val forceUpdateComponentFactory: ForceUpdateComponent.Factory,
|
||||
private val rootDetectedWarningComponentFactory: RootDetectedWarningComponent.Factory,
|
||||
private val rootWarningContinuation: RootWarningContinuation,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val securityInfoProvider: DeviceSecurityInfoProvider,
|
||||
private val appRouterConfig: AppRouterConfig,
|
||||
) : AppComponentContext by context, ComposableContentComponent {
|
||||
|
||||
private val slotNavigation = SlotNavigation<GateConfig>()
|
||||
|
||||
private val slot: Value<ChildSlot<GateConfig, ComposableContentComponent>> = childSlot(
|
||||
source = slotNavigation,
|
||||
serializer = null,
|
||||
handleBackButton = false,
|
||||
childFactory = { config, childContext ->
|
||||
when (config) {
|
||||
is GateConfig.ForceUpdate -> forceUpdateComponentFactory.create(
|
||||
context = childByContext(childContext),
|
||||
params = ForceUpdateComponent.Params(mode = config.mode),
|
||||
)
|
||||
GateConfig.RootWarning -> rootDetectedWarningComponentFactory.create(
|
||||
context = childByContext(childContext),
|
||||
params = Unit,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/** Runs the pre-start gates in order; returns when the app may proceed to normal startup. */
|
||||
suspend fun await() {
|
||||
awaitForceUpdate()
|
||||
awaitRootWarning()
|
||||
}
|
||||
|
||||
private suspend fun awaitForceUpdate() {
|
||||
val mode = runSuspendCatching { resolveForceUpdateMode() }
|
||||
.onFailure { error -> TangemLogger.e("App update check failed, proceeding with normal startup", error) }
|
||||
.getOrNull()
|
||||
?: return
|
||||
|
||||
showGate(GateConfig.ForceUpdate(mode))
|
||||
forceUpdateContinuation.awaitDismiss()
|
||||
slotNavigation.dismiss()
|
||||
}
|
||||
|
||||
private suspend fun awaitRootWarning() {
|
||||
if (settingsRepository.isRootDetectedWarningShown() || !securityInfoProvider.isSecurityExposed()) return
|
||||
|
||||
showGate(GateConfig.RootWarning)
|
||||
rootWarningContinuation.awaitDismiss()
|
||||
settingsRepository.setRootDetectedWarningShown(true)
|
||||
slotNavigation.dismiss()
|
||||
}
|
||||
|
||||
private fun showGate(config: GateConfig) {
|
||||
// The gate overlay is drawn on top of the splash, so mark navigation initialized to dismiss the splash.
|
||||
appRouterConfig.initializedState.value = true
|
||||
slotNavigation.activate(config)
|
||||
}
|
||||
|
||||
private suspend fun resolveForceUpdateMode(): ForceUpdateComponent.Mode? {
|
||||
if (!forceUpdateFeatureToggles.isForceUpdateEnabled) return null
|
||||
|
||||
val mode = getAppUpdateStateUseCase.getCached().toForceUpdateModeOrNull()
|
||||
|
||||
// The force-update screen re-checks on open, so a one-shot refresh is only needed when no screen is shown.
|
||||
if (mode == null) {
|
||||
componentScope.launch { getAppUpdateStateUseCase.refresh() }
|
||||
}
|
||||
|
||||
return mode
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val child by slot.subscribeAsState()
|
||||
child.child?.instance?.Content(modifier)
|
||||
}
|
||||
|
||||
private fun AppUpdateState.toForceUpdateModeOrNull(): ForceUpdateComponent.Mode? = when (this) {
|
||||
AppUpdateState.ForceUpdate -> ForceUpdateComponent.Mode.Force
|
||||
AppUpdateState.Brick -> ForceUpdateComponent.Mode.Brick
|
||||
AppUpdateState.OsTooOld -> ForceUpdateComponent.Mode.OsTooOld
|
||||
AppUpdateState.OptionalUpdate -> ForceUpdateComponent.Mode.Optional
|
||||
AppUpdateState.NoUpdate -> null
|
||||
}
|
||||
|
||||
private sealed interface GateConfig {
|
||||
data class ForceUpdate(val mode: ForceUpdateComponent.Mode) : GateConfig
|
||||
data object RootWarning : GateConfig
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(context: AppComponentContext): AppStartupGateComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -43,6 +43,7 @@ import com.tangem.features.tangempay.components.TangemPayHotWalletOnboardingComp
|
|||
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
|
||||
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.*
|
||||
import com.tangem.features.tokendetails.TokenDetailsComponent
|
||||
import com.tangem.features.virtualaccount.onboarding.component.VirtualAccountOnboardingComponent
|
||||
import com.tangem.features.wallet.WalletEntryComponent
|
||||
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent
|
||||
import com.tangem.features.yield.supply.api.YieldSupplyEntryComponent
|
||||
|
|
@ -71,7 +72,6 @@ internal class ChildFactory @Inject constructor(
|
|||
private val onrampSuccessComponentFactory: OnrampSuccessComponent.Factory,
|
||||
private val buyCryptoComponentFactory: BuyCryptoComponent.Factory,
|
||||
private val sellCryptoComponentFactory: SellCryptoComponent.Factory,
|
||||
private val swapSelectTokensComponentFactory: SwapSelectTokensComponent.Factory,
|
||||
private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory,
|
||||
private val newWelcomeComponentFactory: NewWelcomeComponent.Factory,
|
||||
private val storiesComponentFactory: StoriesComponent.Factory,
|
||||
|
|
@ -114,6 +114,7 @@ internal class ChildFactory @Inject constructor(
|
|||
private val tangemPayDetailsContainerComponentFactory: TangemPayDetailsContainerComponent.Factory,
|
||||
private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory,
|
||||
private val tangemPayWalletOnboardingComponentFactory: TangemPayHotWalletOnboardingComponent.Factory,
|
||||
private val virtualAccountOnboardingComponentFactory: VirtualAccountOnboardingComponent.Factory,
|
||||
private val kycComponentFactory: KycComponent.Factory,
|
||||
private val surveyComponentFactory: SurveyComponent.Factory,
|
||||
private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory,
|
||||
|
|
@ -253,13 +254,6 @@ internal class ChildFactory @Inject constructor(
|
|||
componentFactory = sellCryptoComponentFactory,
|
||||
)
|
||||
}
|
||||
is AppRoute.SwapCrypto -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
params = SwapSelectTokensComponent.Params(userWalletId = route.userWalletId),
|
||||
componentFactory = swapSelectTokensComponentFactory,
|
||||
)
|
||||
}
|
||||
is AppRoute.Onboarding -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
|
|
@ -381,6 +375,7 @@ internal class ChildFactory @Inject constructor(
|
|||
is AppRoute.QrScanning.Source.Send -> SourceType.SEND
|
||||
is AppRoute.QrScanning.Source.WalletConnect -> SourceType.WALLET_CONNECT
|
||||
is AppRoute.QrScanning.Source.MainScreen -> SourceType.MAIN_SCREEN
|
||||
is AppRoute.QrScanning.Source.AddressBook -> SourceType.ADDRESS_BOOK
|
||||
}
|
||||
createComponentChild(
|
||||
context = context,
|
||||
|
|
@ -496,10 +491,14 @@ internal class ChildFactory @Inject constructor(
|
|||
componentFactory = feedEntryComponentFactory,
|
||||
)
|
||||
}
|
||||
is AppRoute.Usedesk -> { // TODO [REDACTED_TASK_KEY] pass params
|
||||
is AppRoute.Usedesk -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
params = UsedeskComponent.Params(),
|
||||
params = UsedeskComponent.Params(
|
||||
userWalletId = route.walletMetaInfo.userWalletId?.stringValue,
|
||||
source = route.source,
|
||||
prefilledMessage = route.prefilledMessage,
|
||||
),
|
||||
componentFactory = usedeskComponentFactory,
|
||||
)
|
||||
}
|
||||
|
|
@ -692,6 +691,7 @@ internal class ChildFactory @Inject constructor(
|
|||
)
|
||||
is AppRoute.TangemPayOnboarding.Mode.FromBannerInSettings -> FromBannerInSettings
|
||||
is AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain -> FromBannerOnMain
|
||||
is AppRoute.TangemPayOnboarding.Mode.MobileOnboardingDeeplink -> MobileOnboardingDeeplink
|
||||
},
|
||||
componentFactory = tangemPayOnboardingComponentFactory,
|
||||
)
|
||||
|
|
@ -703,6 +703,23 @@ internal class ChildFactory @Inject constructor(
|
|||
componentFactory = tangemPayWalletOnboardingComponentFactory,
|
||||
)
|
||||
}
|
||||
is AppRoute.VirtualAccountOnboarding -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
params = when (val mode = route.mode) {
|
||||
is AppRoute.VirtualAccountOnboarding.Mode.Deeplink ->
|
||||
VirtualAccountOnboardingComponent.Params.Deeplink(
|
||||
userWalletId = mode.userWalletId,
|
||||
deeplink = mode.deeplink,
|
||||
)
|
||||
is AppRoute.VirtualAccountOnboarding.Mode.FromMain ->
|
||||
VirtualAccountOnboardingComponent.Params.FromMain(userWalletId = mode.userWalletId)
|
||||
is AppRoute.VirtualAccountOnboarding.Mode.FromDetailsScreen ->
|
||||
VirtualAccountOnboardingComponent.Params.FromDetailsScreen(userWalletId = mode.userWalletId)
|
||||
},
|
||||
componentFactory = virtualAccountOnboardingComponentFactory,
|
||||
)
|
||||
}
|
||||
is AppRoute.Kyc -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
|
|
@ -760,7 +777,7 @@ internal class ChildFactory @Inject constructor(
|
|||
is AppRoute.AddressBook -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
params = AddressBookComponent.Params(route.predefinedAddress),
|
||||
params = AddressBookComponent.Params(addressBookOpenMode = route.addressBookOpenMode),
|
||||
componentFactory = addressBookComponentFactory,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,10 +17,12 @@ import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
|
|||
import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler
|
||||
import com.tangem.features.onramp.deeplink.SellDeepLinkHandler
|
||||
import com.tangem.features.onramp.deeplink.SwapDeepLinkHandler
|
||||
import com.tangem.features.promobanners.api.deeplink.CampaignsDeepLinkHandler
|
||||
import com.tangem.features.send.api.deeplink.SellRedirectDeepLinkHandler
|
||||
import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler
|
||||
import com.tangem.features.survey.deeplink.SurveyDeepLinkHandler
|
||||
import com.tangem.features.tangempay.deeplink.OnboardVisaDeepLinkHandler
|
||||
import com.tangem.features.virtualaccount.onboarding.deeplink.OnboardVirtualAccountsDeepLinkHandler
|
||||
import com.tangem.features.tangempay.deeplink.TangemPayMainDeepLinkHandler
|
||||
import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler
|
||||
import com.tangem.features.wallet.deeplink.PromoDeeplinkHandler
|
||||
|
|
@ -57,6 +59,7 @@ internal class DeepLinkFactory @Inject constructor(
|
|||
private val swapDeepLink: SwapDeepLinkHandler.Factory,
|
||||
private val promoDeepLink: PromoDeeplinkHandler.Factory,
|
||||
private val onboardVisaDeepLink: OnboardVisaDeepLinkHandler.Factory,
|
||||
private val onboardVirtualAccountsDeepLink: OnboardVirtualAccountsDeepLinkHandler.Factory,
|
||||
private val marketsTokenExchangesDeepLink: MarketsTokenExchangesDeepLinkHandler.Factory,
|
||||
private val tangemPayMainDeepLink: TangemPayMainDeepLinkHandler.Factory,
|
||||
private val newsDetailsDeepLink: NewsDetailsDeepLinkHandler.Factory,
|
||||
|
|
@ -64,6 +67,7 @@ internal class DeepLinkFactory @Inject constructor(
|
|||
private val earnDeepLink: EarnDeepLinkHandler.Factory,
|
||||
private val yieldDeepLink: YieldDeepLinkHandler.Factory,
|
||||
private val surveyDeepLink: SurveyDeepLinkHandler.Factory,
|
||||
private val promoCampaignsDeepLink: CampaignsDeepLinkHandler.Factory,
|
||||
) {
|
||||
private val permittedAppRoute = MutableStateFlow(false)
|
||||
|
||||
|
|
@ -173,11 +177,13 @@ internal class DeepLinkFactory @Inject constructor(
|
|||
DeepLinkRoute.WalletConnect.host -> walletConnectDeepLink.create(deeplinkUri)
|
||||
DeepLinkRoute.Promo.host -> promoDeepLink.create(coroutineScope, queryParams)
|
||||
DeepLinkRoute.OnboardVisa.host -> onboardVisaDeepLink.create(deeplinkUri)
|
||||
DeepLinkRoute.OnboardVirtualAccounts.host -> onboardVirtualAccountsDeepLink.create(deeplinkUri)
|
||||
DeepLinkRoute.News.host -> newsDeepLink.create(queryParams)
|
||||
DeepLinkRoute.Earn.host -> earnDeepLink.create(queryParams)
|
||||
DeepLinkRoute.Yield.host -> yieldDeepLink.create(coroutineScope, queryParams)
|
||||
DeepLinkRoute.PayAppMain.host -> tangemPayMainDeepLink.create(coroutineScope, queryParams)
|
||||
DeepLinkRoute.Survey.host -> surveyDeepLink.create(queryParams)
|
||||
DeepLinkRoute.Campaigns.host -> promoCampaignsDeepLink.create(queryParams)
|
||||
else -> {
|
||||
TangemLogger.i(
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue