Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-06 13:39:30 +03:00
commit 92849d43c2
1407 changed files with 64092 additions and 11990 deletions

View file

@ -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),
)
}
}
}

View file

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

View file

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

View file

@ -10,6 +10,7 @@ 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 dagger.Module
@ -31,6 +32,7 @@ internal class TangemSdkManagerModule {
visaCardScanHandler: VisaCardScanHandler,
visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
tangemPayVirtualAccountTaskFactory: TangemPayGenerateVirtualAccountAddressTask.Factory,
onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
analyticsErrorHandler: AnalyticsErrorHandler,
cardRepository: CardRepository,
@ -44,6 +46,7 @@ internal class TangemSdkManagerModule {
visaCardScanHandler = visaCardScanHandler,
visaCardActivationTaskFactory = visaCardActivationTaskFactory,
tangemPayChallengeTaskFactory = tangemPayChallengeTaskFactory,
tangemPayVirtualAccountTaskFactory = tangemPayVirtualAccountTaskFactory,
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
analyticsErrorHandler = analyticsErrorHandler,
cardRepository = cardRepository,

View file

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

View file

@ -1,11 +1,21 @@
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.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.usecase.ValidateContactAddressUseCase
import com.tangem.domain.addressbook.usecase.VerifyAddressEntriesUseCase
import com.tangem.domain.addressbook.usecase.ValidateContactNameUseCase
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.tokens.GetNetworkAddressesUseCase
import com.tangem.domain.transaction.usecase.SignUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
import com.tangem.domain.transaction.usecase.VerifySecp256k1MessagesUseCase
import dagger.Module
@ -32,10 +42,68 @@ object AddressBookDomainModule {
@Provides
@Singleton
fun provideVerifyAddressEntriesUseCase(
fun provideValidateContactNameUseCase(repository: AddressBookRepository): ValidateContactNameUseCase {
return ValidateContactNameUseCase(repository = repository)
}
@Provides
@Singleton
fun provideGetContactsUseCase(repository: AddressBookRepository): GetContactsUseCase {
return GetContactsUseCase(repository = repository)
}
@Provides
@Singleton
fun provideGetVerifiedContactsInteractor(
getContactsUseCase: GetContactsUseCase,
verifyMessagesUseCase: VerifySecp256k1MessagesUseCase,
): VerifyAddressEntriesUseCase {
return VerifyAddressEntriesUseCase(verifyMessagesUseCase = verifyMessagesUseCase)
userWalletsListRepository: UserWalletsListRepository,
): GetVerifiedContactsInteractor {
return GetVerifiedContactsInteractor(
getContacts = getContactsUseCase,
verifyMessages = verifyMessagesUseCase,
userWalletsListRepository = userWalletsListRepository,
)
}
@Provides
@Singleton
fun provideSaveContactInteractor(
repository: AddressBookRepository,
validateContactNameUseCase: ValidateContactNameUseCase,
signUseCase: SignUseCase,
timestampProvider: IsoTimestampProvider,
): SaveContactInteractor {
return SaveContactInteractor(
repository = repository,
validateContactName = validateContactNameUseCase,
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

View file

@ -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 {

View file

@ -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(

View file

@ -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,

View file

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

View file

@ -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
@ -72,6 +73,7 @@ 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,
@ -86,7 +88,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 +104,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 +126,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 {
@ -531,6 +533,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,

View file

@ -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,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -628,5 +628,6 @@ internal class DefaultUserWalletsListRepository(
private suspend fun onAllWalletsDeleted() {
// reset the referral attribution (set from AF deeplink) after removing the last wallet
clearAppsFlyerDeeplinkUseCase(AppsFlyerDeeplinkSource.Referral)
appPreferencesStore.editData { it.remove(PreferencesKeys.USEDESK_CLIENT_ID_KEY) }
}
}

View file

@ -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)

View file

@ -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(),
)

View file

@ -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,

View file

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

View file

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

View file

@ -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()
}

View file

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

View file

@ -43,7 +43,7 @@ internal fun RootContent(
modifier: Modifier = Modifier,
wcContent: @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
@ -82,7 +82,7 @@ internal fun RootContent(
hotAccessCodeContent(Modifier.fillMaxSize())
rootDetectedWarningContent(Modifier.fillMaxSize())
startupGateContent(Modifier.fillMaxSize())
scanFailsContent(Modifier.fillMaxSize())

View file

@ -54,13 +54,13 @@ import com.tangem.sdk.api.BackupServiceHolder
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.analytics.appsflyer.AppsFlyerReferralParamsHandler
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
@ -87,7 +87,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
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,
@ -117,9 +117,8 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
.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 {
@ -175,40 +174,37 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
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)
@ -279,7 +275,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
modifier = modifier,
wcContent = { wcRoutingComponent.Content(it) },
hotAccessCodeContent = { hotAccessCodeRequestComponent.Content(it) },
rootDetectedWarningContent = { rootDetectedWarningComponent.Content(it) },
startupGateContent = { appStartupGateComponent.Content(it) },
scanFailsContent = { scanFailsComponent.Content(it) },
)
}

View file

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

View file

@ -21,7 +21,6 @@ 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
@ -37,12 +36,14 @@ 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.survey.SurveyComponent
import com.tangem.features.swap.SwapComponent
import com.tangem.features.tangempay.components.TangemPayHotWalletOnboardingComponent
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
import com.tangem.features.tangempay.components.TangemPayHotWalletOnboardingComponent
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,
@ -380,6 +374,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,
@ -495,10 +490,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,
)
}
@ -702,6 +701,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,
@ -759,7 +775,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,
)
}

View file

@ -21,6 +21,7 @@ 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 +58,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,
@ -173,6 +175,7 @@ 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)