Updated on 2026-08-14
This commit is contained in:
parent
c707bef9a3
commit
9beb68cc03
7 changed files with 142 additions and 37 deletions
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue