Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-15 22:43:52 +03:00
commit d6f9f59866
1729 changed files with 67614 additions and 9361 deletions

View file

@ -5,9 +5,12 @@ import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.core.analytics.filter.OneTimeEventFilter
import com.tangem.core.analytics.paramsinterceptor.SendTransactionSignerInfoInterceptor
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
import com.tangem.lib.auth.devicekey.DeviceKeyManager
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.lib.auth.AuthFeatureToggles
import com.tangem.lib.auth.session.DeviceRegistrar
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import com.tangem.domain.wallets.repository.WalletsRepository
@ -49,4 +52,10 @@ interface ApplicationEntryPoint {
fun getAppsFlyerClientFactory(): AppsFlyerClient.Factory
fun getSendTransactionSignerInfoInterceptor(): SendTransactionSignerInfoInterceptor
fun getDeviceKeyManager(): DeviceKeyManager
fun getDeviceRegistrar(): DeviceRegistrar
fun getAuthFeatureToggles(): AuthFeatureToggles
}

View file

@ -200,6 +200,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
}
splashScreen.setKeepOnScreenCondition { viewModel.isSplashScreenShown }
splashScreen.setOnExitAnimationListener { provider -> provider.remove() }
installActivityDependencies()
observeAppThemeModeUpdates()

View file

@ -21,6 +21,9 @@ import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.common.LogConfig
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.lib.auth.AuthFeatureToggles
import com.tangem.lib.auth.devicekey.DeviceKeyManager
import com.tangem.lib.auth.session.DeviceRegistrar
import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
@ -92,6 +95,15 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
private val sendTransactionSignerInfoInterceptor
get() = entryPoint.getSendTransactionSignerInfoInterceptor()
private val deviceKeyManager: DeviceKeyManager
get() = entryPoint.getDeviceKeyManager()
private val deviceRegistrar: DeviceRegistrar
get() = entryPoint.getDeviceRegistrar()
private val authFeatureToggles: AuthFeatureToggles
get() = entryPoint.getAuthFeatureToggles()
// endregion
private val appScope = MainScope()
@ -132,6 +144,16 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
}
fun init() {
if (authFeatureToggles.isBackendAuthenticationEnabled) {
appScope.launch {
// Order matters: registration reads the device public key, so it must wait for
// generation to complete. Running them concurrently on first launch would race —
// register() would see `DeviceKeyUnavailable` and defer to the next app launch.
deviceKeyManager.generateIfMissing()
deviceRegistrar.register()
.onLeft { error -> TangemLogger.w("Device registration deferred: $error") }
}
}
walletsRepository = entryPoint.getWalletsRepository()
apiConfigsManager.initialize()

View file

@ -5,6 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.event.SignIn
import com.tangem.domain.card.analytics.IntroductionProcess
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent
class HotWalletContextInterceptor(
val parent: ParamsInterceptor? = null,
@ -18,6 +19,7 @@ class HotWalletContextInterceptor(
is SignIn.ButtonAddWallet,
is SignIn.ButtonUnlockAllWithBiometric,
is IntroductionProcess.ButtonScanCard,
is TokenScreenAnalyticsEvent.ButtonQuickTopUp,
-> false
is SignIn.ErrorBiometricUpdated -> !event.isFromUnlockAll
else -> true

View file

@ -6,6 +6,8 @@ import android.net.Uri
import androidx.core.net.toUri
import com.tangem.common.routing.DeepLinkScheme
import com.tangem.common.uri.ExternalUrlValidator
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.core.navigation.deeplink.DeeplinkLauncher
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.utils.logging.TangemLogger
@ -17,6 +19,7 @@ import com.tangem.utils.logging.TangemLogger
internal class DefaultDeeplinkLauncher(
private val context: Context,
private val urlOpener: UrlOpener,
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
) : DeeplinkLauncher {
override fun launch(link: String) {
@ -58,11 +61,33 @@ internal class DefaultDeeplinkLauncher(
}
private fun launchDeepLink(uri: Uri) {
context.startActivity(createDeepLinkIntent(uri))
val intent = createDeepLinkIntent(uri)
if (intent.resolveActivity(context.packageManager) != null) {
context.startActivity(intent)
} else {
TangemLogger.i(
"""
No match found for deep link
|- Received URI: $uri
""".trimIndent(),
)
analyticsExceptionHandler.sendException(
ExceptionAnalyticsEvent(
exception = UnresolvedDeeplinkException(uri),
params = mapOf(
"uri_scheme" to uri.scheme.orEmpty(),
"uri_host" to uri.host.orEmpty(),
),
),
)
}
}
private fun createDeepLinkIntent(uri: Uri): Intent = Intent(Intent.ACTION_VIEW, uri).apply {
setPackage(context.packageName)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
}
}
internal class UnresolvedDeeplinkException(uri: Uri) :
RuntimeException("Deeplink has no matching activity: scheme=${uri.scheme}, host=${uri.host}")

View file

@ -4,15 +4,20 @@ import android.app.Application
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.tangem.Log
import com.tangem.TangemSdkLogger
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.logs.SensitiveUrlMasker
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
import com.tangem.datasource.utils.WireMockRedirectInterceptor
import com.tangem.domain.common.LogConfig
import com.tangem.operations.attestation.api.TangemApiServiceSettings
import com.tangem.utils.JsonStringValuesExtractor
import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.BuildConfig
import kotlinx.serialization.json.Json
/**
* Owns all app-startup wiring of the logging subsystem in a single place:
@ -23,12 +28,15 @@ import com.tangem.wallet.BuildConfig
* @property appLogsStore app logs store used by file-based writer and the network logs save
* interceptor
* @property tangemSdkLogger Card SDK logger registered with [Log.addLogger]
* @property environmentConfig source of [BlockchainSdkConfig] used to build the blockchain
* URL masker
*
[REDACTED_AUTHOR]
*/
class TangemLoggingInitializer(
private val appLogsStore: AppLogsStore,
private val tangemSdkLogger: TangemSdkLogger,
private val environmentConfig: EnvironmentConfig,
) {
fun initAppLogging() {
@ -64,6 +72,13 @@ class TangemLoggingInitializer(
}
add(createNetworkLoggingInterceptor())
add(ChuckerInterceptor(application))
add(
NetworkLogsSaveInterceptor(
appLogsStore = appLogsStore,
sensitiveUrlMasker = createBlockchainSensitiveUrlMasker(),
shouldCheckResponseBodySize = true,
),
)
}
TangemApiServiceSettings.addInterceptors(
@ -77,4 +92,16 @@ class TangemLoggingInitializer(
}.toTypedArray(),
)
}
private fun createBlockchainSensitiveUrlMasker(): SensitiveUrlMasker {
val json = Json.encodeToJsonElement(
BlockchainSdkConfig.serializer(),
environmentConfig.blockchainSdkConfig,
)
// Drop URL-shaped drawable (e.g. public endpoint URLs from BlockchainSdkConfig like
// kaspaSecondaryApiUrl); they are not secrets and would obscure unrelated requests in logs.
val values = JsonStringValuesExtractor.extract(json)
.filter { it.isNotBlank() && !it.startsWith("http", ignoreCase = true) }
return SensitiveUrlMasker(values)
}
}

View file

@ -1,29 +1,89 @@
package com.tangem.tap.data
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.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.withContext
import java.util.UUID
import java.util.concurrent.TimeUnit
/**
* Default implementation of [OfframpRepository]
* Default implementation of [OfframpRepository].
*
* @property sellService sell service for getting offramp URL
* @property pendingOfframpStore dedicated kotlinx-serialized store of app-initiated sells
* @property dispatchers coroutine dispatchers provider for IO operations
*/
internal class DefaultOfframpRepository(
private val sellService: SellService,
private val pendingOfframpStore: DataStore<List<PendingOfframpEntry>>,
private val dispatchers: CoroutineDispatcherProvider,
) : OfframpRepository {
private val pendingOfframpConverter = PendingOfframpEntryConverter()
override fun getOfframpUrl(
cryptoCurrency: CryptoCurrency,
fiatCurrencyCode: String,
walletAddress: String,
requestId: String,
): String? {
return sellService.getUrl(
cryptoCurrency = cryptoCurrency,
fiatCurrencyName = fiatCurrencyCode,
walletAddress = walletAddress,
isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive,
requestId = requestId,
)
}
override suspend fun registerPendingOfframp(userWalletId: UserWalletId, currencyId: String): String =
withContext(dispatchers.io) {
val requestId = UUID.randomUUID().toString()
val now = System.currentTimeMillis()
pendingOfframpStore.updateData { stored ->
stored.filterNotExpired(now) + PendingOfframpEntry(
requestId = requestId,
userWalletId = userWalletId.stringValue,
currencyId = currencyId,
createdAt = now,
)
}
requestId
}
override suspend fun consumePendingOfframp(
requestId: String,
userWalletId: UserWalletId,
currencyId: String,
): PendingOfframp? = withContext(dispatchers.io) {
val now = System.currentTimeMillis()
var matched: PendingOfframpEntry? = null
pendingOfframpStore.updateData { stored ->
matched = stored.firstOrNull { entry ->
entry.requestId == requestId &&
entry.userWalletId == userWalletId.stringValue &&
entry.currencyId == currencyId &&
now - entry.createdAt < EXPIRY_MS
}
// Remove only the fully-matched record (single-use); always prune expired ones. A request_id that
// matches but with a mismatched wallet/currency is left intact so a tampered redirect cannot burn it.
stored.filter { it != matched }.filterNotExpired(now)
}
matched?.let(pendingOfframpConverter::convert)
}
private fun List<PendingOfframpEntry>.filterNotExpired(now: Long): List<PendingOfframpEntry> =
filter { now - it.createdAt < EXPIRY_MS }
private companion object {
val EXPIRY_MS: Long = TimeUnit.HOURS.toMillis(1)
}
}

View file

@ -267,6 +267,19 @@ internal class DefaultTangemPayStorage @Inject constructor(
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"

View file

@ -0,0 +1,19 @@
package com.tangem.tap.data.converter
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.offramp.model.PendingOfframp
import com.tangem.tap.data.model.PendingOfframpEntry
import com.tangem.utils.converter.Converter
/**
* Converts a persisted [PendingOfframpEntry] into the domain [PendingOfframp].
*/
internal class PendingOfframpEntryConverter : Converter<PendingOfframpEntry, PendingOfframp> {
override fun convert(value: PendingOfframpEntry): PendingOfframp = PendingOfframp(
requestId = value.requestId,
userWalletId = UserWalletId(stringValue = value.userWalletId),
currencyId = value.currencyId,
createdAt = value.createdAt,
)
}

View file

@ -0,0 +1,23 @@
package com.tangem.tap.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Persisted entry of an app-initiated sell (off-ramp) flow, stored in a dedicated kotlinx-serialized DataStore.
*
* [userWalletId] holds the [com.tangem.domain.models.wallet.UserWalletId.stringValue].
*
* @see com.tangem.domain.offramp.model.PendingOfframp
*/
@Serializable
internal data class PendingOfframpEntry(
@SerialName("requestId")
val requestId: String,
@SerialName("userWalletId")
val userWalletId: String,
@SerialName("currencyId")
val currencyId: String,
@SerialName("createdAt")
val createdAt: Long,
)

View file

@ -5,12 +5,10 @@ import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.domain.card.BuildConfig
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
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.product.BlockchainToDeriveFinder
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
import com.tangem.tap.domain.visa.VisaCardScanHandler
@ -34,8 +32,6 @@ internal class TangemSdkManagerModule {
visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
blockchainToDeriveFinder: BlockchainToDeriveFinder,
analyticsErrorHandler: AnalyticsErrorHandler,
cardRepository: CardRepository,
): TangemSdkManager {
@ -49,8 +45,6 @@ internal class TangemSdkManagerModule {
visaCardActivationTaskFactory = visaCardActivationTaskFactory,
tangemPayChallengeTaskFactory = tangemPayChallengeTaskFactory,
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
dynamicAddressesFeatureToggles = dynamicAddressesFeatureToggles,
blockchainToDeriveFinder = blockchainToDeriveFinder,
analyticsErrorHandler = analyticsErrorHandler,
cardRepository = cardRepository,
)

View file

@ -1,6 +1,7 @@
package com.tangem.tap.di
import android.content.Context
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.tap.common.deeplink.DefaultDeeplinkLauncher
import com.tangem.core.navigation.deeplink.DeeplinkLauncher
import com.tangem.core.navigation.finisher.AppFinisher
@ -55,7 +56,10 @@ internal interface UtilsModule {
@Provides
@Singleton
fun provideDeeplinkLauncher(@ApplicationContext context: Context, urlOpener: UrlOpener): DeeplinkLauncher =
DefaultDeeplinkLauncher(context, urlOpener)
fun provideDeeplinkLauncher(
@ApplicationContext context: Context,
urlOpener: UrlOpener,
analyticsExceptionHandler: AnalyticsExceptionHandler,
): DeeplinkLauncher = DefaultDeeplinkLauncher(context, urlOpener, analyticsExceptionHandler)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.di.data
import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.tap.common.log.TangemBlockchainSDKLogger
import com.tangem.tap.common.log.TangemCardSDKLogger
@ -17,10 +18,14 @@ internal object TangemLoggingModule {
@Provides
@Singleton
fun provideLoggingInitializer(appLogsStore: AppLogsStore): TangemLoggingInitializer {
fun provideLoggingInitializer(
appLogsStore: AppLogsStore,
environmentConfig: EnvironmentConfig,
): TangemLoggingInitializer {
return TangemLoggingInitializer(
appLogsStore = appLogsStore,
tangemSdkLogger = TangemCardSDKLogger(appLogsStore),
environmentConfig = environmentConfig,
)
}

View file

@ -0,0 +1,27 @@
package com.tangem.tap.di.domain
import com.tangem.domain.addressbook.usecase.ValidateContactAddressUseCase
import com.tangem.domain.tokens.GetNetworkAddressesUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AddressBookDomainModule {
@Provides
@Singleton
fun provideValidateContactAddressUseCase(
validateWalletAddressUseCase: ValidateWalletAddressUseCase,
getNetworkAddressesUseCase: GetNetworkAddressesUseCase,
): ValidateContactAddressUseCase {
return ValidateContactAddressUseCase(
validateWalletAddressUseCase = validateWalletAddressUseCase,
getNetworkAddressesUseCase = getNetworkAddressesUseCase,
)
}
}

View file

@ -0,0 +1,56 @@
package com.tangem.tap.di.domain
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.tangem.datasource.utils.KotlinxDataStoreSerializer
import com.tangem.domain.offramp.GetOfframpUrlUseCase
import com.tangem.domain.offramp.repository.OfframpRepository
import com.tangem.tap.data.DefaultOfframpRepository
import com.tangem.tap.data.model.PendingOfframpEntry
import com.tangem.tap.network.exchangeServices.SellService
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import kotlinx.serialization.builtins.ListSerializer
@Module
@InstallIn(SingletonComponent::class)
internal object OfframpDomainModule {
@Provides
@Singleton
fun providePendingOfframpStore(
@ApplicationContext context: Context,
appScope: AppCoroutineScope,
): DataStore<List<PendingOfframpEntry>> = DataStoreFactory.create(
serializer = KotlinxDataStoreSerializer(
defaultValue = emptyList(),
serializer = ListSerializer(PendingOfframpEntry.serializer()),
),
produceFile = { context.dataStoreFile(fileName = "pending_offramps") },
scope = appScope,
)
@Provides
@Singleton
fun provideOfframpRepository(
sellService: SellService,
pendingOfframpStore: DataStore<List<PendingOfframpEntry>>,
dispatchers: CoroutineDispatcherProvider,
): OfframpRepository {
return DefaultOfframpRepository(sellService, pendingOfframpStore, dispatchers)
}
@Provides
@Singleton
fun provideGetOfframpUrlUseCase(offrampRepository: OfframpRepository): GetOfframpUrlUseCase {
return GetOfframpUrlUseCase(offrampRepository)
}
}

View file

@ -1,12 +1,8 @@
package com.tangem.tap.di.domain
import com.tangem.domain.offramp.GetOfframpUrlUseCase
import com.tangem.domain.offramp.repository.OfframpRepository
import com.tangem.domain.onramp.*
import com.tangem.domain.onramp.repositories.*
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.tap.data.DefaultOfframpRepository
import com.tangem.tap.network.exchangeServices.SellService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -270,16 +266,4 @@ internal object OnrampDomainModule {
settingsRepository = settingsRepository,
)
}
@Provides
@Singleton
fun provideOfframpRepository(sellService: SellService): OfframpRepository {
return DefaultOfframpRepository(sellService)
}
@Provides
@Singleton
fun provideGetOfframpUrlUseCase(offrampRepository: OfframpRepository): GetOfframpUrlUseCase {
return GetOfframpUrlUseCase(offrampRepository)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.di.domain
import com.tangem.domain.pushnotificationpreferences.ObserveWalletPushNotificationPreferencesUseCase
import com.tangem.domain.pushnotificationpreferences.PreloadWalletPushNotificationPreferencesUseCase
import com.tangem.domain.pushnotificationpreferences.SetAllWalletPushNotificationPreferencesUseCase
import com.tangem.domain.pushnotificationpreferences.UpdateWalletPushNotificationPreferenceUseCase
import com.tangem.domain.pushnotificationpreferences.repository.WalletPushNotificationPreferencesRepository
import dagger.Module
@ -37,4 +38,12 @@ internal object PushNotificationPreferencesDomainModule {
): UpdateWalletPushNotificationPreferenceUseCase {
return UpdateWalletPushNotificationPreferenceUseCase(repository = repository)
}
@Provides
@Singleton
fun providesSetAllWalletPushNotificationPreferencesUseCase(
repository: WalletPushNotificationPreferencesRepository,
): SetAllWalletPushNotificationPreferencesUseCase {
return SetAllWalletPushNotificationPreferencesUseCase(repository = repository)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.di.domain
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.account.status.usecase.IsCryptoCurrencyCouldHideUseCase
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory
import com.tangem.domain.common.wallets.UserWalletsListRepository
@ -10,19 +11,20 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.networks.repository.NetworksRepository
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.stories.StoriesRepository
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.account.status.usecase.IsCryptoCurrencyCouldHideUseCase
import com.tangem.domain.stories.StoriesRepository
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
import com.tangem.domain.tokens.repository.YieldSupplyWarningsViewedRepository
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.domain.virtualaccount.flow.VirtualAccountStatusFetcher
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.features.virtualaccount.VirtualAccountFeatureToggles
import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -162,6 +164,8 @@ internal object TokensDomainModule {
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
virtualAccountStatusFetcher: VirtualAccountStatusFetcher,
virtualAccountsFeatureToggles: VirtualAccountFeatureToggles,
stakingIdFactory: StakingIdFactory,
dispatchers: CoroutineDispatcherProvider,
): WalletBalanceFetcher {
@ -175,6 +179,8 @@ internal object TokensDomainModule {
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
virtualAccountStatusFetcher = virtualAccountStatusFetcher,
virtualAccountsFeatureToggles = virtualAccountsFeatureToggles,
stakingIdFactory = stakingIdFactory,
dispatchers = dispatchers,
)

View file

@ -78,6 +78,16 @@ internal object YieldSupplyDomainModule {
)
}
@Provides
@Singleton
fun provideWrapYieldSwapCallDataWithUpgradeUseCase(
yieldSupplyTransactionRepository: YieldSupplyTransactionRepository,
): WrapYieldSwapCallDataWithUpgradeUseCase {
return WrapYieldSwapCallDataWithUpgradeUseCase(
yieldSupplyTransactionRepository = yieldSupplyTransactionRepository,
)
}
@Provides
@Singleton
fun provideYieldSupplyGetProtocolBalanceUseCase(

View file

@ -27,7 +27,6 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId
@ -58,6 +57,7 @@ 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.utils.logging.TangemLogger
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
@ -73,8 +73,6 @@ internal class DefaultTangemSdkManager(
private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
private val tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
private val blockchainToDeriveFinder: BlockchainToDeriveFinder,
private val analyticsErrorHandler: AnalyticsErrorHandler,
private val cardRepository: CardRepository,
) : TangemSdkManager {
@ -145,12 +143,10 @@ internal class DefaultTangemSdkManager(
runTaskAsyncReturnOnMain(
runnable = ScanProductTask(
card = null,
blockchainToDeriveFinder = blockchainToDeriveFinder,
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
visaCardScanHandler = visaCardScanHandler,
visaCoroutineScope = this,
shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated,
isDynamicAddressesEnabled = dynamicAddressesFeatureToggles.isDynamicAddressesEnabled,
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
cardRepository = cardRepository,
),
@ -242,6 +238,7 @@ internal class DefaultTangemSdkManager(
Analytics.send(event = analyticsEvent.withParams(params.toMap()))
}
.doOnFailure { tangemError ->
TangemLogger.e("scanProduct failed: code=${tangemError.code}, message=${tangemError.customMessage}")
(tangemError as? TangemSdkError)?.let { error ->
Analytics.sendErrorEvent(TangemSdkErrorEvent(error))
}
@ -470,7 +467,6 @@ internal class DefaultTangemSdkManager(
runnable = FinalizeTwinTask(
twinPublicKey = secondCardPublicKey,
issuerKeys = issuerKeyPair,
isDynamicAddressesEnabled = dynamicAddressesFeatureToggles.isDynamicAddressesEnabled,
cardRepository = cardRepository,
),
cardId = cardId,

View file

@ -36,6 +36,9 @@ object MockProvider {
MockOption("Backup Wallet") { BackupWalletMockContent },
MockOption("Dev Wallet") { DevWalletMockContent },
MockOption("Firmware 4.12") { Firmware412MockContent },
MockOption("V3 Multicurrency") { V3MockContent },
MockOption("Single Currency") { SingleCurrencyMockContent },
MockOption("Start2Coin") { S2CMockContent },
MockOption("Cobrand") { showCobrandConfigDialog(it) },
)
@ -99,6 +102,7 @@ object MockProvider {
ProductType.Note -> NoteMockContent
ProductType.Ring -> RingMockContent
ProductType.Twins -> TwinsMockContent
ProductType.Start2Coin -> S2CMockContent
else -> TODO()
}
}

View file

@ -0,0 +1,112 @@
package com.tangem.tap.domain.sdk.mocks.content
import com.tangem.common.SuccessResponse
import com.tangem.common.card.*
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.attestation.Attestation
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.sdk.api.CreateProductWalletTaskResponse
import com.tangem.tap.domain.sdk.mocks.MockContent
import java.util.Date
// Start2Coin (S2C): issuer "Start2Coin" trips isStart2Coin → single currency, WalletConnect hidden.
object S2CMockContent : MockContent {
override val cardDto = CardDTO(
cardId = "1198724260000000",
batchId = "CD04",
cardPublicKey = byteArrayOf(2, 102, 3, -106, -14, -87, -118, 120, 10, 93, 17, 55, 26, -44, 5, 115, 88, 35, 49, -88, -69, 116, 0, -72, -27, 57, 50, -55, 80, -16, 39, -70, 119),
firmwareVersion = CardDTO.FirmwareVersion(
major = 4,
minor = 52,
patch = 0,
type = FirmwareVersion.FirmwareType.Release,
),
manufacturer = CardDTO.Manufacturer(
name = "TANGEM",
manufactureDate = Date(1671494400000),
signature = byteArrayOf(),
),
issuer = CardDTO.Issuer(
name = "Start2Coin",
publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2),
),
settings = CardDTO.Settings(
securityDelay = 15000,
maxWalletsCount = 1,
isSettingAccessCodeAllowed = false,
isSettingPasscodeAllowed = false,
isResettingUserCodesAllowed = true,
isLinkedTerminalEnabled = true,
isBackupAllowed = false,
supportedEncryptionModes = listOf(EncryptionMode.Strong, EncryptionMode.Fast, EncryptionMode.None),
isFilesAllowed = false,
isHDWalletAllowed = false,
isKeysImportAllowed = false,
),
userSettings = CardDTO.UserSettings(isUserCodeRecoveryAllowed = true),
linkedTerminalStatus = CardDTO.LinkedTerminalStatus.None,
isAccessCodeSet = false,
isPasscodeSet = false,
supportedCurves = listOf(EllipticCurve.Secp256k1),
wallets = listOf(
CardDTO.Wallet(
publicKey = byteArrayOf(2, 106, 7, -77, -109, 39, 3, 80, 99, 31, 50, -40, -113, -81, -76, -21, 123, -60, 0, -121, -56, 126, 2, 123, 111, 80, 47, -37, 40, 119, -22, 33, 32),
chainCode = byteArrayOf(),
curve = EllipticCurve.Secp256k1,
settings = CardWallet.Settings(isPermanent = true),
totalSignedHashes = 1,
remainingSignatures = 999999,
index = 0,
hasBackup = false,
derivedKeys = emptyMap(),
extendedPublicKey = null,
isImported = false,
),
),
attestation = Attestation(
cardKeyAttestation = Attestation.Status.Verified,
walletKeysAttestation = Attestation.Status.Skipped,
firmwareAttestation = Attestation.Status.Skipped,
cardUniquenessAttestation = Attestation.Status.Skipped,
),
backupStatus = CardDTO.BackupStatus.NoBackup,
)
override val scanResponse = ScanResponse(
card = cardDto,
productType = ProductType.Start2Coin,
walletData = WalletData(blockchain = "BTC", token = null),
secondTwinPublicKey = null,
derivedKeys = emptyMap(),
primaryCard = null,
)
override val derivationTaskResponse = DerivationTaskResponse(entries = emptyMap())
override val extendedPublicKey
get() = error("Available only for wallet+?")
override val successResponse = SuccessResponse(cardId = "1198724260000000")
override val createProductWalletTaskResponse = CreateProductWalletTaskResponse(
card = cardDto,
derivedKeys = emptyMap(),
primaryCard = null,
)
override val importWalletResponse: CreateProductWalletTaskResponse
get() = error("Available only for Wallet 2")
override val createFirstTwinResponse: CreateWalletResponse
get() = error("Available only for Twin")
override val createSecondTwinResponse: CreateWalletResponse
get() = error("Available only for Twin")
override val finalizeTwinResponse: ScanResponse
get() = error("Available only for Twin")
}

View file

@ -0,0 +1,112 @@
package com.tangem.tap.domain.sdk.mocks.content
import com.tangem.common.SuccessResponse
import com.tangem.common.card.*
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.attestation.Attestation
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.sdk.api.CreateProductWalletTaskResponse
import com.tangem.tap.domain.sdk.mocks.MockContent
import java.util.Date
// Single-currency card (XLM/ed25519, pre-4.0 firmware) → isMultiwalletAllowed false → WalletConnect hidden.
object SingleCurrencyMockContent : MockContent {
override val cardDto = CardDTO(
cardId = "0052000000000000",
batchId = "0052",
cardPublicKey = byteArrayOf(2, 102, 3, -106, -14, -87, -118, 120, 10, 93, 17, 55, 26, -44, 5, 115, 88, 35, 49, -88, -69, 116, 0, -72, -27, 57, 50, -55, 80, -16, 39, -70, 119),
firmwareVersion = CardDTO.FirmwareVersion(
major = 3,
minor = 5,
patch = 0,
type = FirmwareVersion.FirmwareType.Release,
),
manufacturer = CardDTO.Manufacturer(
name = "TANGEM",
manufactureDate = Date(1649635200000),
signature = byteArrayOf(),
),
issuer = CardDTO.Issuer(
name = "TANGEM AG",
publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2),
),
settings = CardDTO.Settings(
securityDelay = 15000,
maxWalletsCount = 1,
isSettingAccessCodeAllowed = false,
isSettingPasscodeAllowed = false,
isResettingUserCodesAllowed = true,
isLinkedTerminalEnabled = true,
isBackupAllowed = false,
supportedEncryptionModes = listOf(EncryptionMode.Strong, EncryptionMode.Fast, EncryptionMode.None),
isFilesAllowed = false,
isHDWalletAllowed = false,
isKeysImportAllowed = false,
),
userSettings = CardDTO.UserSettings(isUserCodeRecoveryAllowed = false),
linkedTerminalStatus = CardDTO.LinkedTerminalStatus.None,
isAccessCodeSet = false,
isPasscodeSet = false,
supportedCurves = listOf(EllipticCurve.Ed25519),
wallets = listOf(
CardDTO.Wallet(
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(),
curve = EllipticCurve.Ed25519,
settings = CardWallet.Settings(isPermanent = false),
totalSignedHashes = 1,
remainingSignatures = null,
index = 0,
hasBackup = false,
derivedKeys = emptyMap(),
extendedPublicKey = null,
isImported = false,
),
),
attestation = Attestation(
cardKeyAttestation = Attestation.Status.Verified,
walletKeysAttestation = Attestation.Status.Skipped,
firmwareAttestation = Attestation.Status.Skipped,
cardUniquenessAttestation = Attestation.Status.Skipped,
),
backupStatus = CardDTO.BackupStatus.NoBackup,
)
override val scanResponse = ScanResponse(
card = cardDto,
productType = ProductType.Wallet,
walletData = WalletData(blockchain = "XLM", token = null),
secondTwinPublicKey = null,
derivedKeys = emptyMap(),
primaryCard = null,
)
override val derivationTaskResponse = DerivationTaskResponse(entries = emptyMap())
override val extendedPublicKey
get() = error("Available only for wallet+?")
override val successResponse = SuccessResponse(cardId = "0052000000000000")
override val createProductWalletTaskResponse = CreateProductWalletTaskResponse(
card = cardDto,
derivedKeys = emptyMap(),
primaryCard = null,
)
override val importWalletResponse: CreateProductWalletTaskResponse
get() = error("Available only for Wallet 2")
override val createFirstTwinResponse: CreateWalletResponse
get() = error("Available only for Twin")
override val createSecondTwinResponse: CreateWalletResponse
get() = error("Available only for Twin")
override val finalizeTwinResponse: ScanResponse
get() = error("Available only for Twin")
}

View file

@ -0,0 +1,112 @@
package com.tangem.tap.domain.sdk.mocks.content
import com.tangem.common.SuccessResponse
import com.tangem.common.card.*
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.attestation.Attestation
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.sdk.api.CreateProductWalletTaskResponse
import com.tangem.tap.domain.sdk.mocks.MockContent
import java.util.Date
// v3 multicurrency card: single secp256k1 wallet on pre-4.0 firmware → isMultiwalletAllowed via the secp branch.
object V3MockContent : MockContent {
override val cardDto = CardDTO(
cardId = "0045000000000000",
batchId = "0045",
cardPublicKey = byteArrayOf(2, 102, 3, -106, -14, -87, -118, 120, 10, 93, 17, 55, 26, -44, 5, 115, 88, 35, 49, -88, -69, 116, 0, -72, -27, 57, 50, -55, 80, -16, 39, -70, 119),
firmwareVersion = CardDTO.FirmwareVersion(
major = 3,
minor = 5,
patch = 0,
type = FirmwareVersion.FirmwareType.Release,
),
manufacturer = CardDTO.Manufacturer(
name = "TANGEM",
manufactureDate = Date(1649635200000),
signature = byteArrayOf(),
),
issuer = CardDTO.Issuer(
name = "TANGEM AG",
publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2),
),
settings = CardDTO.Settings(
securityDelay = 15000,
maxWalletsCount = 1,
isSettingAccessCodeAllowed = false,
isSettingPasscodeAllowed = false,
isResettingUserCodesAllowed = true,
isLinkedTerminalEnabled = true,
isBackupAllowed = false,
supportedEncryptionModes = listOf(EncryptionMode.Strong, EncryptionMode.Fast, EncryptionMode.None),
isFilesAllowed = false,
isHDWalletAllowed = false,
isKeysImportAllowed = false,
),
userSettings = CardDTO.UserSettings(isUserCodeRecoveryAllowed = false),
linkedTerminalStatus = CardDTO.LinkedTerminalStatus.None,
isAccessCodeSet = false,
isPasscodeSet = false,
supportedCurves = listOf(EllipticCurve.Secp256k1),
wallets = listOf(
CardDTO.Wallet(
publicKey = byteArrayOf(2, -27, -117, 23, 68, -3, 21, -109, 18, -67, -107, -42, -44, -16, -127, -53, 46, -109, -46, -51, 89, 119, 79, 111, 78, 62, -125, 72, 109, 8, 45, 59, 117),
chainCode = byteArrayOf(),
curve = EllipticCurve.Secp256k1,
settings = CardWallet.Settings(isPermanent = false),
totalSignedHashes = 1,
remainingSignatures = null,
index = 0,
hasBackup = false,
derivedKeys = emptyMap(),
extendedPublicKey = null,
isImported = false,
),
),
attestation = Attestation(
cardKeyAttestation = Attestation.Status.Verified,
walletKeysAttestation = Attestation.Status.Skipped,
firmwareAttestation = Attestation.Status.Skipped,
cardUniquenessAttestation = Attestation.Status.Skipped,
),
backupStatus = CardDTO.BackupStatus.NoBackup,
)
override val scanResponse = ScanResponse(
card = cardDto,
productType = ProductType.Wallet,
walletData = WalletData(blockchain = "BTC", token = null),
secondTwinPublicKey = null,
derivedKeys = emptyMap(),
primaryCard = null,
)
override val derivationTaskResponse = DerivationTaskResponse(entries = emptyMap())
override val extendedPublicKey
get() = error("Available only for wallet+?")
override val successResponse = SuccessResponse(cardId = "0045000000000000")
override val createProductWalletTaskResponse = CreateProductWalletTaskResponse(
card = cardDto,
derivedKeys = emptyMap(),
primaryCard = null,
)
override val importWalletResponse: CreateProductWalletTaskResponse
get() = error("Available only for Wallet 2")
override val createFirstTwinResponse: CreateWalletResponse
get() = error("Available only for Twin")
override val createSecondTwinResponse: CreateWalletResponse
get() = error("Available only for Twin")
override val finalizeTwinResponse: ScanResponse
get() = error("Available only for Twin")
}

View file

@ -1,74 +0,0 @@
package com.tangem.tap.domain.tasks.product
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.wallets.derivations.BlockchainToDerive
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.tap.features.demo.DemoHelper
import javax.inject.Inject
/**
* Finder of blockchains to derive.
* Returns only saved, default or demo blockchains without any additional logic
* (no cardano/ethereum additions or unnecessary blockchain removals).
*/
class BlockchainToDeriveFinder @Inject constructor(
private val walletAccountsFetcher: WalletAccountsFetcher,
) {
suspend fun find(card: CardDTO): Set<BlockchainToDerive> {
if (!card.settings.isHDWalletAllowed || card.wallets.isEmpty()) return emptySet()
val userWalletId = UserWalletIdBuilder.card(card).build() ?: return emptySet()
val derivationStyle = card.derivationStyleProvider.getDerivationStyle()
val blockchains = getBlockchains(userWalletId).ifEmpty {
if (DemoHelper.isDemoCardId(card.cardId)) {
getDemoBlockchains(derivationStyle, card.cardId)
} else {
getDefaultBlockchains(derivationStyle)
}
}
return blockchains
}
private suspend fun getBlockchains(userWalletId: UserWalletId): Set<BlockchainToDerive> {
return walletAccountsFetcher.getSaved(userWalletId)?.accounts.orEmpty()
.flatMap { accountDTO ->
accountDTO.tokens.orEmpty()
.filter { it.contractAddress == null }
}
.mapNotNull { coin ->
val blockchain = Blockchain.fromNetworkId(coin.networkId) ?: return@mapNotNull null
val derivationPath = coin.derivationPath?.let(::DerivationPath) ?: return@mapNotNull null
BlockchainToDerive(blockchain, derivationPath)
}
.toSet()
}
private fun getDemoBlockchains(derivationStyle: DerivationStyle?, cardId: String): Set<BlockchainToDerive> {
return DemoHelper.config.getDemoBlockchains(cardId).mapToBlockchainsWithDerivations(derivationStyle)
}
private fun getDefaultBlockchains(derivationStyle: DerivationStyle?): Set<BlockchainToDerive> {
val defaultBlockchains = setOf(Blockchain.Bitcoin, Blockchain.Ethereum)
return defaultBlockchains.mapToBlockchainsWithDerivations(derivationStyle)
}
private fun Set<Blockchain>.mapToBlockchainsWithDerivations(
derivationStyle: DerivationStyle?,
): Set<BlockchainToDerive> {
return mapNotNullTo(hashSetOf()) { blockchain ->
val derivationPath = blockchain.derivationPath(derivationStyle) ?: return@mapNotNullTo null
BlockchainToDerive(blockchain, derivationPath)
}
}
}

View file

@ -12,8 +12,6 @@ import com.tangem.common.extensions.*
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvDecoder
import com.tangem.crypto.CryptoUtils
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.wallets.derivations.MissedDerivationsFinder
import com.tangem.domain.card.common.TapWorkarounds.isExcluded
import com.tangem.domain.card.common.TapWorkarounds.isNotSupportedInThatRelease
import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin
@ -32,25 +30,21 @@ import com.tangem.operations.PreflightReadMode
import com.tangem.operations.ScanTask
import com.tangem.operations.backup.PrimaryCard
import com.tangem.operations.backup.StartPrimaryCardLinkingTask
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
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.mainScope
import com.tangem.tap.scope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@Suppress("LongParameterList")
internal class ScanProductTask(
private val card: Card?,
private val blockchainToDeriveFinder: BlockchainToDeriveFinder?,
private val visaCardScanHandler: VisaCardScanHandler?,
private val visaCoroutineScope: CoroutineScope?,
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?,
private val shouldCheckIsAlreadyActivated: Boolean,
private val isDynamicAddressesEnabled: Boolean,
private val cardRepository: CardRepository,
override val allowsRequestAccessCodeFromRepository: Boolean = false,
) : CardSessionRunnable<ScanResponse> {
@ -80,8 +74,6 @@ internal class ScanProductTask(
session = session,
cardDto = cardDto,
scanWalletProcessor = ScanWalletProcessor(
blockchainToDeriveFinder = blockchainToDeriveFinder,
isDynamicAddressesEnabled = isDynamicAddressesEnabled,
cardRepository = cardRepository,
),
callback = callback,
@ -92,8 +84,6 @@ internal class ScanProductTask(
val commandProcessor = when {
cardDto.isTangemTwins -> ScanTwinProcessor()
else -> ScanWalletProcessor(
blockchainToDeriveFinder = blockchainToDeriveFinder,
isDynamicAddressesEnabled = isDynamicAddressesEnabled,
cardRepository = cardRepository,
)
}
@ -102,8 +92,8 @@ internal class ScanProductTask(
is CompletionResult.Success -> ScanTask().run(session) { scanTaskResult ->
when (scanTaskResult) {
is CompletionResult.Success -> {
// it needed because processorResult.data.card doesn't contains attestation result
// and CardWallet.derivedKeys
// It's needed because processorResult.data.card doesn't contain the attestation
// result or the existing CardWallet.derivedKeys read from the card.
val processorScanResponseWithNewCard = processorResult.data.copy(
card = CardDTO(scanTaskResult.data),
)
@ -176,8 +166,6 @@ internal class ScanProductTask(
}
private class ScanWalletProcessor(
private val blockchainToDeriveFinder: BlockchainToDeriveFinder?,
private val isDynamicAddressesEnabled: Boolean,
private val cardRepository: CardRepository,
) : ProductCommandProcessor<ScanResponse> {
@ -281,48 +269,34 @@ private class ScanWalletProcessor(
when (linkingResult) {
is CompletionResult.Success -> {
primaryCard = linkingResult.data
deriveKeysIfNeeded(card, session, callback)
completeScan(card, session, callback)
}
is CompletionResult.Failure -> {
deriveKeysIfNeeded(card, session, callback)
completeScan(card, session, callback)
}
}
}
} else {
deriveKeysIfNeeded(card, session, callback)
completeScan(card, session, callback)
}
}
}
private fun deriveKeysIfNeeded(
// Keys are no longer derived during scan: default derivations are created up front in
// CreateProductWalletTask, and derivations for additional tokens are handled by
// DefaultColdMapDerivationsRepository when the user explicitly adds a token.
private fun completeScan(
card: CardDTO,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
val productType = getWalletProductType(card)
scope.launch {
val scanResponse = ScanResponse(
card = card,
productType = productType,
walletData = session.environment.walletData,
primaryCard = primaryCard,
)
val derivations = collectDerivations(card, scanResponse)
if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) {
callback(CompletionResult.Success(scanResponse))
return@launch
}
DeriveMultipleWalletPublicKeysTask(derivations).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
val response = scanResponse.copy(derivedKeys = result.data.entries)
callback(CompletionResult.Success(response))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
val scanResponse = ScanResponse(
card = card,
productType = getWalletProductType(card),
walletData = session.environment.walletData,
primaryCard = primaryCard,
)
callback(CompletionResult.Success(scanResponse))
}
private fun getWalletProductType(card: CardDTO): ProductType {
@ -334,17 +308,6 @@ private class ScanWalletProcessor(
else -> ProductType.Wallet
}
}
private suspend fun collectDerivations(
card: CardDTO,
scanResponse: ScanResponse,
): Map<ByteArrayKey, List<DerivationPath>> {
val blockchains = blockchainToDeriveFinder
?.find(card)
?: return emptyMap()
return MissedDerivationsFinder(scanResponse, isDynamicAddressesEnabled).findByBlockchainsToDerive(blockchains)
}
}
@Suppress("MagicNumber")

View file

@ -13,7 +13,6 @@ import com.tangem.tap.domain.tasks.product.ScanProductTask
class FinalizeTwinTask(
private val twinPublicKey: ByteArray,
private val issuerKeys: KeyPair,
private val isDynamicAddressesEnabled: Boolean,
private val cardRepository: CardRepository,
) : CardSessionRunnable<ScanResponse> {
@ -31,11 +30,9 @@ class FinalizeTwinTask(
is CompletionResult.Success ->
ScanProductTask(
card = readResult.data,
blockchainToDeriveFinder = null,
visaCardScanHandler = null,
visaCoroutineScope = null,
shouldCheckIsAlreadyActivated = false,
isDynamicAddressesEnabled = isDynamicAddressesEnabled,
onboardingV2FeatureToggles = null,
cardRepository = cardRepository,
).run(session, callback)

View file

@ -242,6 +242,10 @@ internal class DefaultUserWalletsListRepository(
setSelectedUserWallet(newSelected)
}
userWallets.value = updatedWallets
if (updatedWallets?.isEmpty() == true) {
trackingContextProxy.eraseContext()
}
}
@Suppress("CyclomaticComplexMethod", "LongMethod")
@ -325,11 +329,7 @@ internal class DefaultUserWalletsListRepository(
sensitiveInformationRepository.getAll(listOf(encryptionKey))
.doOnSuccess { sensitiveInfo ->
updateWallets { wallets ->
// It is necessary to update derivations because when scanning we obtain the missing keys
wallets?.updateWith(
walletIdToSensitiveInformation = sensitiveInfo,
walletIdToDerivedKeys = mapOf(userWallet.walletId to scanResponse.derivedKeys),
)
wallets?.updateWith(walletIdToSensitiveInformation = sensitiveInfo)
}
trackSignInEvent(userWallet, AnalyticsParam.SignInType.Card)
}

View file

@ -1,10 +1,8 @@
package com.tangem.tap.domain.userWalletList.utils
import com.tangem.domain.models.scan.KeyWalletPublicKey
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
@ -74,10 +72,7 @@ internal fun List<UserWalletPublicInformation>.toUserWallets(): List<UserWallet>
return this.map { it.toUserWallet() }
}
internal fun UserWallet.updateWith(
sensitiveInformation: UserWalletSensitiveInformation,
derivedKeys: Map<KeyWalletPublicKey, ExtendedPublicKeysMap>?,
): UserWallet {
internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInformation): UserWallet {
return when (this) {
is UserWallet.Cold -> {
copy(
@ -85,7 +80,6 @@ internal fun UserWallet.updateWith(
card = scanResponse.card.copy(
wallets = requireNotNull(sensitiveInformation.wallets),
),
derivedKeys = derivedKeys ?: scanResponse.derivedKeys,
// visaCardActivationStatus = sensitiveInformation.visaCardActivationStatus,
),
)
@ -98,17 +92,14 @@ internal fun UserWallet.updateWith(
internal fun List<UserWallet>.updateWith(
walletIdToSensitiveInformation: Map<UserWalletId, UserWalletSensitiveInformation>,
walletIdToDerivedKeys: Map<UserWalletId, Map<KeyWalletPublicKey, ExtendedPublicKeysMap>>? = null,
): List<UserWallet> {
return if (walletIdToSensitiveInformation.isEmpty()) {
this
} else {
this.map { wallet ->
val sensitiveInformation = walletIdToSensitiveInformation[wallet.walletId]
val derivedKeys = walletIdToDerivedKeys?.get(wallet.walletId)
if (sensitiveInformation != null) {
wallet.updateWith(sensitiveInformation, derivedKeys)
wallet.updateWith(sensitiveInformation)
} else {
wallet
}

View file

@ -17,6 +17,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -29,6 +30,7 @@ import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.AppCurrencySelectorScreenTestTags
import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorState.Currency
import com.tangem.wallet.R
import kotlinx.collections.immutable.ImmutableList
@ -123,7 +125,9 @@ private fun TopBar(
when (state) {
is AppCurrencySelectorState.Content -> {
IconButton(
modifier = Modifier.size(TangemTheme.dimens.size32),
modifier = Modifier
.size(TangemTheme.dimens.size32)
.testTag(AppCurrencySelectorScreenTestTags.TOP_BAR_ACTION_BUTTON),
onClick = state.onTopBarActionClick,
) {
val iconResId = when (state) {
@ -157,7 +161,8 @@ private fun SearchBar(onInputChange: (String) -> Unit, modifier: Modifier = Modi
TextField(
modifier = modifier
.focusRequester(focusRequester),
.focusRequester(focusRequester)
.testTag(AppCurrencySelectorScreenTestTags.SEARCH_FIELD),
value = input,
onValueChange = { input = it },
singleLine = true,
@ -218,7 +223,7 @@ private fun CurrenciesList(
) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
LazyColumn(
modifier = modifier,
modifier = modifier.testTag(AppCurrencySelectorScreenTestTags.LAZY_LIST),
state = listState,
contentPadding = PaddingValues(bottom = bottomBarHeight),
) {
@ -246,6 +251,7 @@ private fun CurrencyItem(name: String, isSelected: Boolean, onClick: () -> Unit,
Row(
modifier = modifier
.testTag(AppCurrencySelectorScreenTestTags.CURRENCY_ITEM)
.clickable(
interactionSource = interactionSource,
indication = LocalIndication.current,

View file

@ -10,11 +10,13 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.AppSettingsScreenTestTags
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
import com.tangem.tap.features.details.ui.appsettings.components.*
@ -55,7 +57,15 @@ private fun AppSettings(state: AppSettingsScreenState.Content) {
item = item,
)
is Item.Button -> SettingsButtonItem(
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8),
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing8)
.then(
if (item.id == AppSettingsItemsFactory.ID_SELECT_APP_CURRENCY_BUTTON) {
Modifier.testTag(AppSettingsScreenTestTags.CURRENCY_BUTTON)
} else {
Modifier
},
),
item = item,
)
is Item.Switch -> SettingsSwitchItem(

View file

@ -1,8 +1,11 @@
package com.tangem.tap.features.hot
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.model.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
@ -16,7 +19,9 @@ import javax.inject.Singleton
* Be aware that the SDK is initialized on activity creation, so it may not be available immediately.
*/
@Singleton
class TangemHotSDKProxy @Inject constructor() : TangemHotSdk {
class TangemHotSDKProxy @Inject constructor(
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
) : TangemHotSdk {
val sdkState = MutableStateFlow<TangemHotSdk?>(null)
@ -56,8 +61,15 @@ class TangemHotSDKProxy @Inject constructor() : TangemHotSdk {
callSdk { signHashes(unlockHotWallet, dataToSign) }
private suspend fun <T> callSdk(block: suspend TangemHotSdk.() -> T): T {
return withTimeout(timeMillis = 1000) {
sdkState.filterNotNull().first()
}.block()
return try {
withTimeout(timeMillis = 1000) {
sdkState.filterNotNull().first()
}.block()
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
analyticsExceptionHandler.sendException(ExceptionAnalyticsEvent(exception = e))
throw e
}
}
}

View file

@ -60,6 +60,7 @@ internal class DefaultAuthProvider(
override fun getGaslessServiceApiKey(apiEnvironment: Provider<ApiEnvironment>): ProviderSuspend<String> {
return ProviderSuspend {
when (apiEnvironment.invoke()) {
ApiEnvironment.MOCK,
ApiEnvironment.DEV,
-> environmentConfig.gaslessTxApiKeyDev
ApiEnvironment.PROD -> environmentConfig.gaslessTxApiKey

View file

@ -20,5 +20,6 @@ interface SellService {
fiatCurrencyName: String,
walletAddress: String,
isDarkTheme: Boolean,
requestId: String,
): String?
}

View file

@ -138,6 +138,7 @@ class MoonPayService(
fiatCurrencyName: String,
walletAddress: String,
isDarkTheme: Boolean,
requestId: String,
): String? {
val blockchain = cryptoCurrency.network.toBlockchain()
if (blockchain.isTestnet()) return blockchain.getTestnetTopUpUrl()
@ -165,7 +166,12 @@ class MoonPayService(
.appendQueryParameter("apiKey", apiKey)
.appendQueryParameter("baseCurrencyCode", moonpayCurrency.currencyCode.uppercase())
.appendQueryParameter("refundWalletAddress", walletAddress)
.appendQueryParameter("redirectURL", "tangem://redirect_sell?currency_id=${cryptoCurrency.id.value}")
// request_id authenticates the returning redirect_sell deeplink. It must be added to
// redirectURL BEFORE createSignature below so it is covered by the MoonPay URL signature.
.appendQueryParameter(
"redirectURL",
"tangem://redirect_sell?currency_id=${cryptoCurrency.id.value}&request_id=$requestId",
)
if (isDarkTheme) uri.appendQueryParameter("theme", "dark")

View file

@ -4,12 +4,7 @@ import android.app.Activity
import android.os.Build
import android.os.Bundle
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
@ -32,11 +27,7 @@ import com.tangem.core.ui.components.haze.ProvideHaze
import com.tangem.core.ui.components.snackbar.TangemSnackbarHost
import com.tangem.core.ui.components.snackbar.TangemTopSnackbarHost
import com.tangem.core.ui.message.EventMessageEffect
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.LocalRootBackgroundColor
import com.tangem.core.ui.res.LocalSnackbarHostState
import com.tangem.core.ui.res.LocalTopSnackbarHostState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.*
import com.tangem.core.ui.security.ProvideSecureFlagController
import com.tangem.tap.routing.component.RoutingComponent
import com.tangem.tap.routing.transitions.RoutingTransitionAnimationFactory
@ -124,20 +115,34 @@ private fun childrenAnimation(
backHandler: BackHandler,
onBack: () -> Unit,
): StackAnimation<AppRoute, RoutingComponent.Child> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
val routeAnimation = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
predictiveBackAnimation(
backHandler = backHandler,
onBack = onBack,
selector = { backEvent, _, _ ->
androidPredictiveBackAnimatable(backEvent)
},
fallbackAnimation = stackAnimation {
RoutingTransitionAnimationFactory.create(it.configuration)
fallbackAnimation = stackAnimation<AppRoute, RoutingComponent.Child> { child ->
RoutingTransitionAnimationFactory.create(child.configuration)
},
)
} else {
stackAnimation {
RoutingTransitionAnimationFactory.create(it.configuration)
stackAnimation { child ->
RoutingTransitionAnimationFactory.create(child.configuration)
}
}
return skipAnimationWhileInitial(routeAnimation)
}
private fun skipAnimationWhileInitial(
delegate: StackAnimation<AppRoute, RoutingComponent.Child>,
): StackAnimation<AppRoute, RoutingComponent.Child> = StackAnimation { stack, animModifier, content ->
if (stack.active.configuration is AppRoute.Initial) {
Box(modifier = animModifier) {
content(stack.active)
}
} else {
delegate(stack, animModifier, content)
}
}

View file

@ -214,33 +214,18 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
FeatureToggles.AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING,
)
TangemLogger.i("[TangemPay][HWO] Feature toggle enabled=$isHotWalletOnboardingEnabled")
if (isHotWalletOnboardingEnabled) {
val afterEmptyRoute: AppRoute = if (isHotWalletOnboardingEnabled) {
val tangemPayHotWalletOnboardingDeepLink = withTimeoutOrNull(2.seconds) {
appsFlyerReferralParamsHandler.waitForDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding)
}
TangemLogger.i("[TangemPay][HWO] Deep link present=${tangemPayHotWalletOnboardingDeepLink != null}")
if (tangemPayHotWalletOnboardingDeepLink != null) {
val hotWalletRoute = AppRoute.TangemPayHotWalletOnboarding
val shouldShowTos = !cardRepository.isTangemTOSAccepted()
val route = if (shouldShowTos) "Disclaimer" else "HotWalletOnboarding"
TangemLogger.i("[TangemPay][HWO] TOS accepted=${!shouldShowTos}, navigating to $route")
return if (shouldShowTos) {
AppRoute.Disclaimer(isTosAccepted = false, nextRoute = hotWalletRoute)
} else {
hotWalletRoute
}
AppRoute.TangemPayHotWalletOnboarding
} else {
getDefaultRoute()
}
}
val isHideStoriesForReferralEnabled = featureTogglesManager.isFeatureEnabled(
FeatureToggles.TWI_1512_HIDE_STORIES_FOR_REFERRAL_ENABLED,
)
// Referral users skip the Home stories screen and land directly on the
// mobile wallet creation flow.
val afterEmptyRoute: AppRoute = if (isHideStoriesForReferralEnabled && shouldShowMobileWalletPromoUseCase()) {
AppRoute.CreateWalletStart(mode = AppRoute.CreateWalletStart.Mode.HotWallet)
} else {
AppRoute.Home(launchMode = launchMode)
getDefaultRoute()
}
val shouldAskPushPermission = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrNull()
@ -261,6 +246,19 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
}
}
private suspend fun getDefaultRoute(): AppRoute {
val isHideStoriesForReferralEnabled = featureTogglesManager.isFeatureEnabled(
FeatureToggles.TWI_1512_HIDE_STORIES_FOR_REFERRAL_ENABLED,
)
// Referral users skip the Home stories screen and land directly on the
// mobile wallet creation flow.
return if (isHideStoriesForReferralEnabled && shouldShowMobileWalletPromoUseCase()) {
AppRoute.CreateWalletStart(mode = AppRoute.CreateWalletStart.Mode.HotWallet)
} else {
AppRoute.Home(launchMode = launchMode)
}
}
@Composable
override fun Content(modifier: Modifier) {
RootContent(
@ -397,7 +395,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
componentScope.launch(dispatchers.main) {
backupServiceHolder.backupService.get()?.discardSavedBackup()
val unfinishedBackup = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch
cardRepository.finishCardActivation(unfinishedBackup.card.cardId)
cardRepository.finishCardActivation(cardId = unfinishedBackup.card.cardId, hasBackupError = true)
onboardingRepository.clearUnfinishedFinalizeOnboarding()
analyticsEventHandler.send(OnboardingAnalyticsEvent.Onboarding.Finished())
}

View file

@ -9,9 +9,9 @@ import com.tangem.feature.stories.api.StoriesComponent
import com.tangem.feature.usedesk.api.UsedeskComponent
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
import com.tangem.features.account.AccountCreateEditComponent
import com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent
import com.tangem.features.account.AccountDetailsComponent
import com.tangem.features.account.ArchivedAccountListComponent
import com.tangem.features.addressbook.AddressBookComponent
import com.tangem.features.createwalletselection.CreateWalletSelectionComponent
import com.tangem.features.createwalletstart.CreateWalletStartComponent
import com.tangem.features.details.component.DetailsComponent
@ -21,6 +21,7 @@ import com.tangem.features.feed.entry.components.FeedEntryRoute
import com.tangem.features.home.api.HomeComponent
import com.tangem.features.hotwallet.*
import com.tangem.features.kyc.KycComponent
import com.tangem.features.survey.SurveyComponent
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensMode
@ -31,9 +32,10 @@ import com.tangem.features.onramp.component.*
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacksStub
import com.tangem.features.pushnotifications.api.PushNotificationsParams
import com.tangem.features.send.v2.api.NFTSendComponent
import com.tangem.features.send.v2.api.SendComponent
import com.tangem.features.send.v2.api.SendEntryPointComponent
import com.tangem.features.pushnotificationsettings.component.PushNotificationSettingsComponent
import com.tangem.features.send.api.NFTSendComponent
import com.tangem.features.send.api.SendComponent
import com.tangem.features.send.api.SendEntryPointComponent
import com.tangem.features.staking.api.StakingComponent
import com.tangem.features.swap.SwapComponent
import com.tangem.features.tangempay.components.TangemPayHotWalletOnboardingComponent
@ -86,6 +88,7 @@ internal class ChildFactory @Inject constructor(
private val resetCardComponentFactory: ResetCardComponent.Factory,
private val referralComponentFactory: ReferralComponent.Factory,
private val pushNotificationsComponentFactory: PushNotificationsComponent.Factory,
private val pushNotificationSettingsComponentFactory: PushNotificationSettingsComponent.Factory,
private val walletComponentFactory: WalletEntryComponent.Factory,
private val sendComponentFactoryV2: SendComponent.Factory,
private val redesignedWalletConnectComponentFactory: WalletConnectEntryComponent.Factory,
@ -112,9 +115,10 @@ internal class ChildFactory @Inject constructor(
private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory,
private val tangemPayWalletOnboardingComponentFactory: TangemPayHotWalletOnboardingComponent.Factory,
private val kycComponentFactory: KycComponent.Factory,
private val surveyComponentFactory: SurveyComponent.Factory,
private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory,
private val feedEntryComponentFactory: FeedEntryComponent.Factory,
private val addFundsComponentFactory: AddFundsComponent.Factory,
private val addressBookComponentFactory: AddressBookComponent.Factory,
) {
@Suppress("LongMethod", "CyclomaticComplexMethod")
@ -170,6 +174,13 @@ internal class ChildFactory @Inject constructor(
componentFactory = walletSettingsComponentFactory,
)
}
is AppRoute.PushNotificationSettings -> {
createComponentChild(
context = context,
params = PushNotificationSettingsComponent.Params(route.userWalletId),
componentFactory = pushNotificationSettingsComponentFactory,
)
}
is AppRoute.WalletBackup -> {
createComponentChild(
context = context,
@ -216,6 +227,7 @@ internal class ChildFactory @Inject constructor(
userWalletId = route.userWalletId,
cryptoCurrency = route.currency,
source = route.source,
initialFiatAmount = route.initialFiatAmount,
),
componentFactory = onrampComponentFactory,
)
@ -234,13 +246,6 @@ internal class ChildFactory @Inject constructor(
componentFactory = buyCryptoComponentFactory,
)
}
is AppRoute.AddFunds -> {
createComponentChild(
context = context,
params = AddFundsComponent.Params(userWalletId = route.userWalletId),
componentFactory = addFundsComponentFactory,
)
}
is AppRoute.SellCrypto -> {
createComponentChild(
context = context,
@ -702,6 +707,13 @@ internal class ChildFactory @Inject constructor(
componentFactory = kycComponentFactory,
)
}
is AppRoute.Survey -> {
createComponentChild(
context = context,
params = SurveyComponent.Params(token = route.token, displayId = route.displayId),
componentFactory = surveyComponentFactory,
)
}
is AppRoute.YieldSupplyEntry -> {
createComponentChild(
context = context,
@ -742,6 +754,13 @@ internal class ChildFactory @Inject constructor(
componentFactory = feedEntryComponentFactory,
)
}
is AppRoute.AddressBook -> {
createComponentChild(
context = context,
params = AddressBookComponent.Params(route.predefinedAddress),
componentFactory = addressBookComponentFactory,
)
}
}
}
}

View file

@ -17,8 +17,9 @@ 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.send.v2.api.deeplink.SellRedirectDeepLinkHandler
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.tangempay.deeplink.TangemPayMainDeepLinkHandler
import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler
@ -62,6 +63,7 @@ internal class DeepLinkFactory @Inject constructor(
private val newsDeepLink: NewsDeepLinkHandler.Factory,
private val earnDeepLink: EarnDeepLinkHandler.Factory,
private val yieldDeepLink: YieldDeepLinkHandler.Factory,
private val surveyDeepLink: SurveyDeepLinkHandler.Factory,
) {
private val permittedAppRoute = MutableStateFlow(false)
@ -175,6 +177,7 @@ internal class DeepLinkFactory @Inject constructor(
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)
else -> {
TangemLogger.i(
"""