Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-14 17:31:35 +04:00
parent 7e6461b073
commit 79fefb8300
4 changed files with 57 additions and 6 deletions

View file

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

View file

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

View file

@ -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<UserWallet>) {
if (!authFeatureToggles.isBackendAuthenticationEnabled) return
userWallets.filterIsInstance<UserWallet.Hot>().forEach { registerMobile(it) }
userWallets.asSequence()
.filterIsInstance<UserWallet.Hot>()
.filter { it.hotWalletId.authType == HotWalletId.AuthType.NoPassword }
.forEach { registerMobile(it) }
}
private fun UserWalletId.toBase64(): String = Base64.encodeToString(value, Base64.NO_WRAP)

View file

@ -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<UserWallet.Cold>()))
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<UserWallet.Cold>(), // 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<HotWalletId> { every { this@mockk.authType } returns authType }
val wallet = mockk<UserWallet.Hot>()
every { wallet.walletId } returns UserWalletId(value = walletIdValue)
every { wallet.hotWalletId } returns hotWalletId
return wallet
}
}