Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-22 13:29:39 +03:00
parent c707bef9a3
commit 9beb68cc03
7 changed files with 142 additions and 37 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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