From 79fefb83008cda4b7a8b9e998232a07c790d3dee Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 14 Jul 2026 17:31:35 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../com/tangem/tap/ApplicationEntryPoint.kt | 6 ++++++ .../java/com/tangem/tap/TangemApplication.kt | 21 +++++++++++++++++++ .../WalletRegistrationLauncher.kt | 17 ++++++++++++--- .../WalletRegistrationLauncherTest.kt | 19 ++++++++++++++--- 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 61f52257be..a2a9869feb 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -12,11 +12,13 @@ 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.common.wallets.UserWalletsListRepository import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerClient import com.tangem.tap.common.log.TangemLoggingInitializer +import com.tangem.tap.domain.walletregistration.WalletRegistrationLauncher import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent @@ -58,4 +60,8 @@ interface ApplicationEntryPoint { fun getDeviceRegistrar(): DeviceRegistrar fun getAuthFeatureToggles(): AuthFeatureToggles + + fun getWalletRegistrationLauncher(): WalletRegistrationLauncher + + fun getUserWalletsListRepository(): UserWalletsListRepository } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 1a1a4046bf..4c11c044a6 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -20,6 +20,7 @@ import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.domain.apptheme.GetAppThemeModeUseCase import com.tangem.domain.common.LogConfig +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.lib.auth.AuthFeatureToggles import com.tangem.lib.auth.devicekey.DeviceKeyManager @@ -34,6 +35,8 @@ import com.tangem.tap.common.analytics.handlers.customerio.CustomerIoAnalyticsHa import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler import com.tangem.tap.common.images.createCoilImageLoader import com.tangem.tap.common.log.TangemLoggingInitializer +import com.tangem.tap.domain.walletregistration.WalletRegistrationLauncher +import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.utils.logging.TangemLogger import com.tangem.wallet.BuildConfig import dagger.hilt.EntryPoints @@ -104,6 +107,12 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. private val authFeatureToggles: AuthFeatureToggles get() = entryPoint.getAuthFeatureToggles() + private val walletRegistrationLauncher: WalletRegistrationLauncher + get() = entryPoint.getWalletRegistrationLauncher() + + private val userWalletsListRepository: UserWalletsListRepository + get() = entryPoint.getUserWalletsListRepository() + // endregion private val appScope = MainScope() @@ -152,6 +161,18 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. deviceKeyManager.generateIfMissing() deviceRegistrar.register() .onLeft { error -> TangemLogger.w("Device registration deferred: $error") } + .onRight { + // Safety net: register any MOBILE wallets that missed it (created before the + // toggle was on, or after a transient failure). Requires the DPoP session from + // device registration, so it runs only once that succeeded. Best-effort — + // loading the wallet list (userWalletsSync) can throw, which must not crash + // startup, so the whole retry is guarded. + runSuspendCatching { + walletRegistrationLauncher.retryMobileRegistrations( + userWalletsListRepository.userWalletsSync(), + ) + }.onFailure { error -> TangemLogger.e("Mobile wallet registration retry failed", error) } + } } } walletsRepository = entryPoint.getWalletsRepository() diff --git a/app/src/main/java/com/tangem/tap/domain/walletregistration/WalletRegistrationLauncher.kt b/app/src/main/java/com/tangem/tap/domain/walletregistration/WalletRegistrationLauncher.kt index 459f5955d8..3ee0bac060 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletregistration/WalletRegistrationLauncher.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletregistration/WalletRegistrationLauncher.kt @@ -7,6 +7,7 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.hot.sdk.model.HotWalletId import com.tangem.lib.auth.AuthFeatureToggles import com.tangem.lib.auth.session.WalletRegistrar import com.tangem.utils.coroutines.AppCoroutineScope @@ -21,7 +22,7 @@ import javax.inject.Inject * never blocks the user). MOBILE wallets register without UI; COLD wallets attest inside a live * card session (no extra tap) and POST after the session closes. */ -internal class WalletRegistrationLauncher @Inject constructor( +class WalletRegistrationLauncher @Inject internal constructor( private val walletRegistrar: WalletRegistrar, private val mobileSigner: MobileWalletRegistrationSigner, private val coldSigner: ColdWalletRegistrationSigner, @@ -71,11 +72,21 @@ internal class WalletRegistrationLauncher @Inject constructor( } } - /** Launch-time safety net: registers any not-yet-registered MOBILE wallets (no UI). */ + /** + * Launch-time safety net: registers not-yet-registered MOBILE wallets without any UI. + * + * Only wallets that can sign **silently** are retried — i.e. [HotWalletId.AuthType.NoPassword]. + * Password/Biometry wallets would pop an unlock prompt (see `DefaultHotWalletAccessor`), which + * must never happen at startup; those are left to register when a real unlock context exists + * (e.g. on creation, or the next time the user unlocks them). + */ suspend fun retryMobileRegistrations(userWallets: List) { if (!authFeatureToggles.isBackendAuthenticationEnabled) return - userWallets.filterIsInstance().forEach { registerMobile(it) } + userWallets.asSequence() + .filterIsInstance() + .filter { it.hotWalletId.authType == HotWalletId.AuthType.NoPassword } + .forEach { registerMobile(it) } } private fun UserWalletId.toBase64(): String = Base64.encodeToString(value, Base64.NO_WRAP) diff --git a/app/src/test/java/com/tangem/tap/domain/walletregistration/WalletRegistrationLauncherTest.kt b/app/src/test/java/com/tangem/tap/domain/walletregistration/WalletRegistrationLauncherTest.kt index 3de81302ff..dcf13ec8ae 100644 --- a/app/src/test/java/com/tangem/tap/domain/walletregistration/WalletRegistrationLauncherTest.kt +++ b/app/src/test/java/com/tangem/tap/domain/walletregistration/WalletRegistrationLauncherTest.kt @@ -4,6 +4,7 @@ import arrow.core.right import com.google.common.truth.Truth.assertThat import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.hot.sdk.model.HotWalletId import com.tangem.lib.auth.AuthFeatureToggles import com.tangem.lib.auth.session.WalletRegistrar import com.tangem.lib.auth.session.WalletSigner @@ -75,17 +76,29 @@ internal class WalletRegistrationLauncherTest { } @Test - fun `retryMobileRegistrations registers only hot wallets`() = runTest { + fun `retryMobileRegistrations registers only silently-signable hot wallets`() = runTest { every { authFeatureToggles.isBackendAuthenticationEnabled } returns true - launcher.retryMobileRegistrations(listOf(hotWallet(), mockk())) + launcher.retryMobileRegistrations( + listOf( + hotWallet(authType = HotWalletId.AuthType.NoPassword), // registered — signs silently + hotWallet(authType = HotWalletId.AuthType.Password), // skipped — would prompt + hotWallet(authType = HotWalletId.AuthType.Biometry), // skipped — would prompt + mockk(), // skipped — not a hot wallet + ), + ) coVerify(exactly = 1) { walletRegistrar.register(any(), any()) } } - private fun hotWallet(walletIdValue: ByteArray = ByteArray(32) { 1 }): UserWallet.Hot { + private fun hotWallet( + walletIdValue: ByteArray = ByteArray(32) { 1 }, + authType: HotWalletId.AuthType = HotWalletId.AuthType.NoPassword, + ): UserWallet.Hot { + val hotWalletId = mockk { every { this@mockk.authType } returns authType } val wallet = mockk() every { wallet.walletId } returns UserWalletId(value = walletIdValue) + every { wallet.hotWalletId } returns hotWalletId return wallet } } \ No newline at end of file