From 9beb68cc03817b731a2406d290cf85fd937de985 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 22 Dec 2025 13:29:39 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../accesscode/AccessCodeComponent.kt | 1 + .../hotwallet/accesscode/AccessCodeModel.kt | 94 ++++++++++++------- .../accesscode/entity/AccessCodeUM.kt | 1 + .../hotwallet/accesscode/ui/AccessCode.kt | 57 +++++++++++ .../entry/AddExistingWalletModel.kt | 11 ++- .../updateaccesscode/UpdateAccessCodeModel.kt | 4 + .../entry/WalletActivationModel.kt | 11 ++- 7 files changed, 142 insertions(+), 37 deletions(-) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt index 08693225cc..b9badf2e25 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt @@ -53,6 +53,7 @@ internal class AccessCodeComponent @AssistedInject constructor( interface ModelCallbacks { fun onNewAccessCodeInput(userWalletId: UserWalletId, accessCode: String) + fun onAccessCodeUpdateStarted(userWalletId: UserWalletId) fun onAccessCodeUpdated(userWalletId: UserWalletId) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt index 13f1a7ab8f..1da4a1c296 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt @@ -16,6 +16,7 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.hotwallet.IsAccessCodeSimpleUseCase import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.requireHotWallet import com.tangem.domain.settings.CanUseBiometryUseCase import com.tangem.domain.settings.SetAskBiometryShownUseCase import com.tangem.domain.settings.ShouldShowAskBiometryUseCase @@ -31,6 +32,7 @@ import com.tangem.hot.sdk.model.HotAuth import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -80,6 +82,7 @@ internal class AccessCodeModel @Inject constructor( accessCode = "", accessCodeColor = PinTextColor.Primary, onAccessCodeChange = ::onAccessCodeChange, + isLoading = false, isConfirmMode = params.accessCodeToConfirm != null, ) @@ -117,6 +120,18 @@ internal class AccessCodeModel @Inject constructor( } } + private suspend fun setLoadingIfLongJob(job: Job) { + delay(timeMillis = 2000) + + if (job.isActive) { + uiState.update { currentState -> + currentState.copy( + isLoading = true, + ) + } + } + } + private fun onNewCodeSet() { modelScope.launch { delay(timeMillis = SUCCESS_DISPLAY_DURATION_MS) @@ -187,56 +202,65 @@ internal class AccessCodeModel @Inject constructor( } private fun setCode(userWalletId: UserWalletId, accessCode: String) { + params.callbacks.onAccessCodeUpdateStarted(params.userWalletId) + modelScope.launch { val userWallet = getUserWalletUseCase(userWalletId) .getOrElse { error("User wallet with id $userWalletId not found") } - if (userWallet !is UserWallet.Hot) return@launch + .requireHotWallet() tryToAskForBiometry() - userWalletsListRepository.setLock( - userWallet.walletId, - UserWalletsListRepository.LockMethod.AccessCode(accessCode.toCharArray()), - ) - - if (walletsRepository.useBiometricAuthentication()) { - userWalletsListRepository.setLock( - userWallet.walletId, - UserWalletsListRepository.LockMethod.Biometric, - ) + val settingCodeJob = launch(dispatchers.main) { + setCodeOperation(userWallet, accessCode) + params.callbacks.onAccessCodeUpdated(params.userWalletId) } - val unlockHotWallet = getHotWalletContextualUnlockUseCase(userWallet.hotWalletId) - .getOrNull() - ?: run { - require(userWallet.hotWalletId.authType == HotWalletId.AuthType.NoPassword) { - "Something went wrong. Hot wallet is locked and cannot be unlocked with NoAuth" - } + setLoadingIfLongJob(settingCodeJob) + } + } - hotWalletAccessor.unlockContextual(userWallet.hotWalletId) + private suspend fun setCodeOperation(userWallet: UserWallet.Hot, accessCode: String) { + userWalletsListRepository.setLock( + userWalletId = userWallet.walletId, + lockMethod = UserWalletsListRepository.LockMethod.AccessCode(accessCode.toCharArray()), + ) + + if (walletsRepository.useBiometricAuthentication()) { + userWalletsListRepository.setLock( + userWalletId = userWallet.walletId, + lockMethod = UserWalletsListRepository.LockMethod.Biometric, + ) + } + + val unlockHotWallet = getHotWalletContextualUnlockUseCase(userWallet.hotWalletId) + .getOrNull() + ?: run { + require(userWallet.hotWalletId.authType == HotWalletId.AuthType.NoPassword) { + "Something went wrong. Hot wallet is locked and cannot be unlocked with NoAuth" } - var updatedHotWalletId = tangemHotSdk.changeAuth( - unlockHotWallet = unlockHotWallet, - auth = HotAuth.Password(accessCode.toCharArray()), - ) - - if (walletsRepository.requireAccessCode().not()) { - updatedHotWalletId = tangemHotSdk.changeAuth( - unlockHotWallet = unlockHotWallet, - auth = HotAuth.Biometry, - ) + hotWalletAccessor.unlockContextual(userWallet.hotWalletId) } - userWalletsListRepository.saveWithoutLock( - userWallet.copy(hotWalletId = updatedHotWalletId), - canOverride = true, + var updatedHotWalletId = tangemHotSdk.changeAuth( + unlockHotWallet = unlockHotWallet, + auth = HotAuth.Password(accessCode.toCharArray()), + ) + + if (walletsRepository.requireAccessCode().not()) { + updatedHotWalletId = tangemHotSdk.changeAuth( + unlockHotWallet = unlockHotWallet, + auth = HotAuth.Biometry, ) - - clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId) - - params.callbacks.onAccessCodeUpdated(params.userWalletId) } + + userWalletsListRepository.saveWithoutLock( + userWallet.copy(hotWalletId = updatedHotWalletId), + canOverride = true, + ) + + clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId) } @OptIn(ExperimentalCoroutinesApi::class) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt index 978071789b..5b4302d4bf 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt @@ -8,6 +8,7 @@ import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH internal data class AccessCodeUM( val accessCode: String, val accessCodeColor: PinTextColor, + val isLoading: Boolean, val onAccessCodeChange: (String) -> Unit, val isConfirmMode: Boolean, val requestFocus: StateEvent = consumedEvent(), diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt index ecfa44d9d6..f92f63bdfe 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt @@ -3,8 +3,10 @@ package com.tangem.features.hotwallet.accesscode.ui import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -16,9 +18,14 @@ import com.tangem.core.res.R import com.tangem.core.ui.components.fields.PinTextColor import com.tangem.core.ui.components.fields.PinTextField import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.haptic.TangemHapticEffect +import com.tangem.core.ui.res.LocalHapticManager import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch @Suppress("LongParameterList", "LongMethod") @Composable @@ -27,6 +34,24 @@ internal fun AccessCode( modifier: Modifier = Modifier, focusRequester: FocusRequester = remember { FocusRequester() }, ) { + if (state.isLoading) { + Box( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.primary) + .navigationBarsPadding(), + ) { + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.Center) + .size(20.dp), + color = TangemTheme.colors.icon.secondary, + strokeWidth = 2.dp, + ) + } + return + } + Column( modifier = modifier .fillMaxSize() @@ -88,6 +113,20 @@ internal fun AccessCode( } } } + + val hapticManager = LocalHapticManager.current + + LaunchedEffect(state.accessCodeColor) { + when (state.accessCodeColor) { + PinTextColor.WrongCode -> { + launch(NonCancellable) { + delay(timeMillis = 500) + hapticManager.perform(TangemHapticEffect.View.Reject) + } + } + else -> Unit + } + } } @Preview(showBackground = true, widthDp = 360) @@ -101,6 +140,7 @@ private fun PreviewSet() { accessCodeColor = PinTextColor.Primary, onAccessCodeChange = {}, isConfirmMode = false, + isLoading = false, ), ) } @@ -117,6 +157,23 @@ private fun PreviewConfirm() { accessCodeColor = PinTextColor.Success, onAccessCodeChange = {}, isConfirmMode = true, + isLoading = false, + ), + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun PreviewConfirmLoading() { + TangemThemePreview { + AccessCode( + state = AccessCodeUM( + accessCode = "123456", + accessCodeColor = PinTextColor.Success, + onAccessCodeChange = {}, + isConfirmMode = true, + isLoading = true, ), ) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt index ed1d37d3f9..dc5afd1b9f 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt @@ -123,12 +123,16 @@ internal class AddExistingWalletModel @Inject constructor( } inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback { + var canSkip = true + override fun onBackClick() { onChildBack() } override fun onSkipClick() { - showSkipAccessCodeWarningDialog() + if (canSkip) { + showSkipAccessCodeWarningDialog() + } } } @@ -157,7 +161,12 @@ internal class AddExistingWalletModel @Inject constructor( stackNavigation.push(AddExistingWalletRoute.ConfirmAccessCode(userWalletId, accessCode)) } + override fun onAccessCodeUpdateStarted(userWalletId: UserWalletId) { + hotWalletStepperComponentModelCallback.canSkip = false + } + override fun onAccessCodeUpdated(userWalletId: UserWalletId) { + hotWalletStepperComponentModelCallback.canSkip = true navigateToPushNotificationsOrNext() } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt index cd562b219b..e671521f5d 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt @@ -50,6 +50,10 @@ internal class UpdateAccessCodeModel @Inject constructor( stackNavigation.push(UpdateAccessCodeRoute.ConfirmAccessCode(userWalletId, accessCode)) } + override fun onAccessCodeUpdateStarted(userWalletId: UserWalletId) { + // No-op + } + override fun onAccessCodeUpdated(userWalletId: UserWalletId) { stackNavigation.push(UpdateAccessCodeRoute.SetupFinished) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt index 129947fdb7..2a1f014021 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt @@ -156,12 +156,16 @@ internal class WalletActivationModel @Inject constructor( } inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback { + var canSkip = true + override fun onBackClick() { onChildBack() } override fun onSkipClick() { - showSkipAccessCodeWarningDialog() + if (canSkip) { + showSkipAccessCodeWarningDialog() + } } } @@ -220,8 +224,13 @@ internal class WalletActivationModel @Inject constructor( stackNavigation.push(WalletActivationRoute.ConfirmAccessCode(accessCode)) } + override fun onAccessCodeUpdateStarted(userWalletId: UserWalletId) { + hotWalletStepperComponentModelCallback.canSkip = false + } + override fun onAccessCodeUpdated(userWalletId: UserWalletId) { navigateToPushNotificationsOrNext() + hotWalletStepperComponentModelCallback.canSkip = true } }