Updated on 2026-08-14

This commit is contained in:
Tangem 2023-03-06 20:32:39 +03:00
parent c2aef02443
commit 605d7da00d
28 changed files with 415 additions and 205 deletions

View file

@ -117,6 +117,8 @@ object TangemSdkErrorMapper {
is TangemSdkError.BiometricsAuthenticationLockout -> error
is TangemSdkError.BiometricsAuthenticationPermanentLockout -> error
is TangemSdkError.UserCanceledBiometricsAuthentication -> error
is TangemSdkError.EncryptionOperationFailed -> error
is TangemSdkError.InvalidEncryptionKey -> error
}
}
}

View file

@ -1,16 +0,0 @@
package com.tangem.tap.domain.userWalletList
import com.tangem.common.core.TangemError
import com.tangem.wallet.R
sealed class UserWalletListError(code: Int) : TangemError(code) {
override val silent: Boolean
get() = (cause as? TangemError)?.silent == true
override val messageResId: Int? = null
object WalletAlreadySaved : UserWalletListError(code = 60001) {
override var customMessage: String = "This wallet has already been saved, you can add another one"
override val messageResId: Int = R.string.user_wallet_list_error_wallet_already_saved
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.tap.domain.userWalletList
import com.tangem.common.core.TangemError
import com.tangem.wallet.R
sealed class UserWalletsListError(code: Int) : TangemError(code) {
override val silent: Boolean
get() = (cause as? TangemError)?.silent == true
override val messageResId: Int? = null
object WalletAlreadySaved : UserWalletsListError(code = 60001) {
override var customMessage: String = "This wallet has already been saved, you can add another one"
override val messageResId: Int = R.string.user_wallet_list_error_wallet_already_saved
}
object InvalidEncryptionKey : UserWalletsListError(code = 60002) {
override var customMessage: String = "Invalid encryption key"
}
data class BiometricsAuthenticationLockout(val isPermanent: Boolean) : UserWalletsListError(code = 60003) {
override var customMessage: String = "Biometric authentication lockout, permanent: $isPermanent"
}
data class UnableToUnlockUserWallets(override val cause: Throwable? = null) : UserWalletsListError(code = 60004) {
override var customMessage: String = "An error has occurred, please scan your card to log in"
override val messageResId: Int = R.string.user_wallet_list_error_unable_to_unlock
}
}

View file

@ -39,7 +39,7 @@ interface UserWalletsListManager {
/**
* Save provided user wallet and set it as selected
* @param userWallet [UserWallet] to save
* @param canOverride If false, then terminate with [UserWalletListError.WalletAlreadySaved] when user tries
* @param canOverride If false, then terminate with [UserWalletsListError.WalletAlreadySaved] when user tries
* to save an already saved card
* @return [CompletionResult] of operation
*/
@ -47,7 +47,7 @@ interface UserWalletsListManager {
/**
* Same as [save] but not change selected user wallet ID
* and not terminate with [UserWalletListError.WalletAlreadySaved] if [UserWallet] already saved
* and not terminate with [UserWalletsListError.WalletAlreadySaved] if [UserWallet] already saved
*
* Can terminate with [NoSuchElementException] if unable to find [UserWallet] with provided [UserWalletId]
* @param userWalletId update [UserWallet] with that [UserWalletId]
@ -108,7 +108,7 @@ interface UserWalletsListManager {
* @return [CompletionResult] of operation, with selected [UserWallet]
* or null if there is no selected [UserWallet]
* */
suspend fun unlock(): CompletionResult<UserWallet?>
suspend fun unlock(): CompletionResult<UserWallet>
/**
* Remove [UserWallet]s from [userWallets] and set [isLocked] as true

View file

@ -16,6 +16,8 @@ val UserWalletsListManager.isLockable: Boolean
*
* @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable] returns [Flow] which
* produces only one false value
*
* @see UserWalletsListManager.Lockable.isLockedSync
* */
val UserWalletsListManager.isLocked: Flow<Boolean>
get() = asLockable()?.isLocked ?: flowOf(false)
@ -24,6 +26,8 @@ val UserWalletsListManager.isLocked: Flow<Boolean>
* Indicates that the [UserWalletsListManager] is locked
*
* @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable] returns false
*
* @see UserWalletsListManager.Lockable.isLockedSync
* */
val UserWalletsListManager.isLockedSync: Boolean
get() = asLockable()?.isLockedSync ?: false
@ -32,15 +36,22 @@ val UserWalletsListManager.isLockedSync: Boolean
* Call [UserWalletsListManager.Lockable.unlock] if [UserWalletsListManager] implements [UserWalletsListManager.Lockable]
*
* @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable]
* returns [CompletionResult.Success] with [UserWalletsListManager.selectedUserWalletSync]
* returns [CompletionResult.Failure] with [UserWalletsListError.UnableToUnlockUserWallets]
*
* If [UserWalletsListManager] implements [UserWalletsListManager.Lockable]
* returns [CompletionResult.Success] with selected [UserWallet]
*
* @see UserWalletsListManager.Lockable.unlock
* */
suspend fun UserWalletsListManager.unlockIfLockable(): CompletionResult<UserWallet?> {
return asLockable()?.unlock() ?: CompletionResult.Success(selectedUserWalletSync)
suspend fun UserWalletsListManager.unlockIfLockable(): CompletionResult<UserWallet> {
return asLockable()?.unlock() ?: CompletionResult.Failure(UserWalletsListError.UnableToUnlockUserWallets())
}
/**
* Call [UserWalletsListManager.Lockable.lock] if [UserWalletsListManager] implements [UserWalletsListManager.Lockable]
* or do nothing otherwise
*
* @see UserWalletsListManager.Lockable.lock
* */
fun UserWalletsListManager.lockIfLockable() {
asLockable()?.lock()

View file

@ -1,9 +1,10 @@
package com.tangem.tap.domain.userWalletList.implementation
import com.tangem.common.*
import com.tangem.common.extensions.guard
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.userWalletList.UserWalletListError
import com.tangem.tap.domain.userWalletList.UserWalletsListError
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
import com.tangem.tap.domain.userWalletList.repository.SelectedUserWalletRepository
@ -15,7 +16,6 @@ import com.tangem.tap.domain.userWalletList.utils.toUserWallets
import com.tangem.tap.domain.userWalletList.utils.updateWith
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import timber.log.Timber
@OptIn(ExperimentalCoroutinesApi::class)
internal class BiometricUserWalletsListManager(
@ -53,9 +53,32 @@ internal class BiometricUserWalletsListManager(
override val hasUserWallets: Boolean
get() = keysRepository.hasSavedEncryptionKeys()
override suspend fun unlock(): CompletionResult<UserWallet?> {
override suspend fun unlock(): CompletionResult<UserWallet> {
// Occurs when user has locked user wallets after unlocking with biometry
// e.g. after biometric storage master key invalidation
if (state.value.hasLockedUserWalletsAfterUnlock) {
return CompletionResult.Failure(
error = UserWalletsListError.UnableToUnlockUserWallets(
cause = IllegalStateException("Permanently locked"),
),
)
}
return unlockWithBiometryInternal()
.map { selectedUserWalletSync }
.mapFailure { error ->
if (error is UserWalletsListError) {
error
} else {
UserWalletsListError.UnableToUnlockUserWallets(cause = error)
}
}
.map {
selectedUserWalletSync.guard {
throw UserWalletsListError.UnableToUnlockUserWallets(
cause = IllegalStateException("No user wallet selected"),
)
}
}
}
override fun lock() {
@ -88,7 +111,7 @@ internal class BiometricUserWalletsListManager(
}
if (isWalletSaved) {
CompletionResult.Failure(UserWalletListError.WalletAlreadySaved)
CompletionResult.Failure(UserWalletsListError.WalletAlreadySaved)
} else {
saveInternal(userWallet, changeSelectedUserWallet = true)
}
@ -188,8 +211,10 @@ internal class BiometricUserWalletsListManager(
.flatMap { loadModels() }
.map {
state.update { prevState ->
val hasLockedUserWallets = prevState.userWallets.any { it.isLocked }
prevState.copy(
isLocked = false,
isLocked = hasLockedUserWallets,
hasLockedUserWalletsAfterUnlock = hasLockedUserWallets,
)
}
}
@ -234,7 +259,13 @@ internal class BiometricUserWalletsListManager(
}
}
.doOnFailure { error ->
Timber.e(error, "Unable to load user wallets")
if (error is UserWalletsListError.InvalidEncryptionKey) {
state.update { prevState ->
prevState.copy(
hasLockedUserWalletsAfterUnlock = true,
)
}
}
}
}
@ -290,7 +321,7 @@ internal class BiometricUserWalletsListManager(
private fun findSelectedUserWallet(userWallets: List<UserWallet> = state.value.userWallets): UserWallet? {
return userWallets.firstOrNull {
it.walletId == state.value.selectedUserWalletId
it.walletId == state.value.selectedUserWalletId && !it.isLocked
}
}
@ -299,5 +330,6 @@ internal class BiometricUserWalletsListManager(
val userWallets: List<UserWallet> = emptyList(),
val selectedUserWalletId: UserWalletId? = null,
val isLocked: Boolean = true,
val hasLockedUserWalletsAfterUnlock: Boolean = false,
)
}

View file

@ -4,7 +4,7 @@ import com.tangem.common.CompletionResult
import com.tangem.common.catching
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.userWalletList.UserWalletListError
import com.tangem.tap.domain.userWalletList.UserWalletsListError
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
@ -49,7 +49,7 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager {
val isWalletSaved = state.value.userWallet?.walletId == userWallet.walletId
if (isWalletSaved) {
CompletionResult.Failure(UserWalletListError.WalletAlreadySaved)
CompletionResult.Failure(UserWalletsListError.WalletAlreadySaved)
} else {
saveInternal(userWallet)
}

View file

@ -8,10 +8,13 @@ import com.tangem.common.biometric.BiometricManager
import com.tangem.common.biometric.BiometricStorage
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.flatMapOnFailure
import com.tangem.common.fold
import com.tangem.common.map
import com.tangem.common.mapFailure
import com.tangem.common.services.secure.SecureStorage
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.domain.userWalletList.UserWalletsListError
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository
import kotlinx.coroutines.Dispatchers
@ -37,6 +40,16 @@ internal class BiometricUserWalletsKeysRepository(
override suspend fun getAll(): CompletionResult<List<UserWalletEncryptionKey>> {
return withContext(Dispatchers.IO) {
getAllInternal()
.mapFailure { error ->
when (error) {
is TangemSdkError.BiometricsAuthenticationLockout ->
UserWalletsListError.BiometricsAuthenticationLockout(isPermanent = false)
is TangemSdkError.BiometricsAuthenticationPermanentLockout ->
UserWalletsListError.BiometricsAuthenticationLockout(isPermanent = true)
is TangemSdkError.InvalidEncryptionKey -> UserWalletsListError.InvalidEncryptionKey
else -> error
}
}
}
}
@ -79,12 +92,28 @@ internal class BiometricUserWalletsKeysRepository(
return getUserWalletsIds()
.map { userWalletId ->
// This is possible because the Card SDK cipher key has an expiration time
// If this operation runs more than that expiration time, the user will not receive all encryption keys
// If this operation runs more than that expiration time, the user will have to re-authorize
// to receive all keys
getEncryptionKey(userWalletId)
.flatMapOnFailure { error ->
// If key decryption failed then skip it
if (error is TangemSdkError.EncryptionOperationFailed) {
CompletionResult.Success(data = null)
} else {
CompletionResult.Failure(error)
}
}
.doOnFailure { error ->
// If the user cancels biometric authentication, cancel the request for all keys
if (error is TangemSdkError.UserCanceledBiometricsAuthentication) {
return CompletionResult.Failure(error)
when (error) {
is TangemSdkError.UserCanceledBiometricsAuthentication -> {
// If the user cancels biometric authentication, cancel the request for all keys
return CompletionResult.Failure(error)
}
is TangemSdkError.InvalidEncryptionKey -> {
if (error.isKeyRegenerated) {
deleteEncryptionKey(userWalletId)
}
}
}
}
}
@ -94,20 +123,20 @@ internal class BiometricUserWalletsKeysRepository(
}
private suspend fun getEncryptionKey(userWalletId: UserWalletId): CompletionResult<UserWalletEncryptionKey?> {
return biometricStorage.get(StorageKey.WalletEncryptionKey(userWalletId).name)
return biometricStorage.get(StorageKey.UserWalletEncryptionKey(userWalletId).name)
.map { it.decodeToKey() }
}
private suspend fun storeEncryptionKey(encryptionKey: UserWalletEncryptionKey): CompletionResult<Unit> {
return biometricStorage.store(
key = StorageKey.WalletEncryptionKey(encryptionKey.walletId).name,
key = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name,
data = encryptionKey.encode(),
)
.map { storeUserWalletId(encryptionKey.walletId) }
}
private suspend fun deleteEncryptionKey(userWalletId: UserWalletId): CompletionResult<Unit> {
return biometricStorage.delete(StorageKey.WalletEncryptionKey(userWalletId).name)
return biometricStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name)
}
private suspend fun getUserWalletsIds(): List<UserWalletId> {
@ -175,7 +204,7 @@ internal class BiometricUserWalletsKeysRepository(
private sealed interface StorageKey {
val name: String
class WalletEncryptionKey(userWalletId: UserWalletId) : StorageKey {
class UserWalletEncryptionKey(userWalletId: UserWalletId) : StorageKey {
override val name: String = "user_wallet_encryption_key_${userWalletId.stringValue}"
}

View file

@ -25,11 +25,13 @@ import com.tangem.core.ui.fragments.ComposeBottomSheetFragment
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.analytics.events.MyWallets
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
import com.tangem.tap.features.walletSelector.ui.components.BiometricsLockoutDialogContent
import com.tangem.tap.features.walletSelector.ui.components.BiometricsLockoutWarningContent
import com.tangem.tap.features.walletSelector.ui.components.KeyInvalidatedWarningContent
import com.tangem.tap.features.walletSelector.ui.components.RemoveWalletDialogContent
import com.tangem.tap.features.walletSelector.ui.components.RenameWalletDialogContent
import com.tangem.tap.features.walletSelector.ui.components.WalletSelectorScreenContent
import com.tangem.tap.features.walletSelector.ui.model.DialogModel
import com.tangem.tap.features.walletSelector.ui.model.WarningModel
internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment<WalletSelectorScreenState>() {
private val viewModel by viewModels<WalletSelectorViewModel>()
@ -93,7 +95,8 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment<Wa
when (dialog) {
is DialogModel.RemoveWalletDialog -> RemoveWalletDialogContent(dialog)
is DialogModel.RenameWalletDialog -> RenameWalletDialogContent(dialog)
is DialogModel.BiometricsLockoutDialog -> BiometricsLockoutDialogContent(dialog)
is WarningModel.BiometricsLockoutWarning -> BiometricsLockoutWarningContent(dialog)
is WarningModel.KeyInvalidatedWarning -> KeyInvalidatedWarningContent(dialog)
}
}
}

View file

@ -3,11 +3,11 @@ package com.tangem.tap.features.walletSelector.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.common.analytics.events.MyWallets
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.domain.userWalletList.UserWalletsListError
import com.tangem.tap.domain.userWalletList.isLocked
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction
@ -15,6 +15,7 @@ import com.tangem.tap.features.walletSelector.redux.WalletSelectorState
import com.tangem.tap.features.walletSelector.ui.model.DialogModel
import com.tangem.tap.features.walletSelector.ui.model.MultiCurrencyUserWalletItem
import com.tangem.tap.features.walletSelector.ui.model.SingleCurrencyUserWalletItem
import com.tangem.tap.features.walletSelector.ui.model.WarningModel
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletStoresManager
@ -159,7 +160,7 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
}
}
}
val biometricsLockoutDialog = createBiometricsLockoutDialogIfNeeded(state.error, prevState.dialog)
val warningDialog = createWarningDialogIfNeeded(state.error, prevState.dialog)
prevState.copy(
multiCurrencyWallets = multiCurrencyWallets,
@ -169,9 +170,9 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
isLocked = state.isLocked,
showUnlockProgress = state.isUnlockInProgress,
showAddCardProgress = state.isCardSavingInProgress,
dialog = biometricsLockoutDialog,
dialog = warningDialog,
error = state.error
?.takeIf { !it.silent && biometricsLockoutDialog == null }
?.takeIf { !it.silent && warningDialog == null }
?.let { error ->
error.messageResId?.let { TextReference.Res(it) }
?: TextReference.Str(error.customMessage)
@ -184,24 +185,23 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
store.unsubscribe(this)
}
private fun createBiometricsLockoutDialogIfNeeded(
private fun createWarningDialogIfNeeded(
error: TangemError?,
currentDialogModel: DialogModel?,
currentDialog: DialogModel?,
): DialogModel? {
return when (error) {
is TangemSdkError.BiometricsAuthenticationLockout -> DialogModel.BiometricsLockoutDialog(
isPermanent = false,
onDismiss = this::dismissDialog,
is UserWalletsListError.BiometricsAuthenticationLockout -> WarningModel.BiometricsLockoutWarning(
isPermanent = error.isPermanent,
onDismiss = this::dismissWarningDialog,
)
is TangemSdkError.BiometricsAuthenticationPermanentLockout -> DialogModel.BiometricsLockoutDialog(
isPermanent = true,
onDismiss = this::dismissDialog,
is UserWalletsListError.InvalidEncryptionKey -> WarningModel.KeyInvalidatedWarning(
onDismiss = this::dismissWarningDialog,
)
else -> currentDialogModel
else -> currentDialog
}
}
private fun dismissDialog() {
private fun dismissWarningDialog() {
stateInternal.update { prevState ->
prevState.copy(
dialog = null,

View file

@ -8,26 +8,26 @@ import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.walletSelector.ui.model.DialogModel
import com.tangem.tap.features.walletSelector.ui.model.WarningModel
import com.tangem.wallet.R
@Composable
internal fun BiometricsLockoutDialogContent(
dialog: DialogModel.BiometricsLockoutDialog,
internal fun BiometricsLockoutWarningContent(
warning: WarningModel.BiometricsLockoutWarning,
) {
BasicDialog(
title = stringResource(id = R.string.biometric_lockout_warning_title),
message = stringResource(
id = if (dialog.isPermanent) {
id = if (warning.isPermanent) {
R.string.biometric_lockout_permanent_warning_description
} else {
R.string.biometric_lockout_warning_description
},
),
onDismissDialog = dialog.onDismiss,
onDismissDialog = warning.onDismiss,
confirmButton = DialogButton(
title = stringResource(id = R.string.common_ok),
onClick = dialog.onDismiss,
onClick = warning.onDismiss,
),
)
}
@ -38,8 +38,8 @@ private fun BiometricsLockoutDialogSample(
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
BiometricsLockoutDialogContent(
dialog = DialogModel.BiometricsLockoutDialog(
BiometricsLockoutWarningContent(
warning = WarningModel.BiometricsLockoutWarning(
isPermanent = false,
onDismiss = {},
),
@ -68,8 +68,8 @@ private fun BiometricsLockoutDialog_Permanent_Sample(
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
BiometricsLockoutDialogContent(
dialog = DialogModel.BiometricsLockoutDialog(
BiometricsLockoutWarningContent(
warning = WarningModel.BiometricsLockoutWarning(
isPermanent = true,
onDismiss = {},
),

View file

@ -0,0 +1,56 @@
package com.tangem.tap.features.walletSelector.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.walletSelector.ui.model.WarningModel
import com.tangem.wallet.R
@Composable
internal fun KeyInvalidatedWarningContent(
warning: WarningModel.KeyInvalidatedWarning,
) {
BasicDialog(
title = stringResource(id = R.string.common_attention),
message = stringResource(id = R.string.key_invalidated_warning_description),
onDismissDialog = warning.onDismiss,
confirmButton = DialogButton(
title = stringResource(id = R.string.common_ok),
onClick = warning.onDismiss,
),
)
}
// region Preview
@Composable
private fun KeyInvalidatedWarningSample(
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
KeyInvalidatedWarningContent(
warning = WarningModel.KeyInvalidatedWarning(onDismiss = {}),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun KeyInvalidatedWarningPreview_Light() {
TangemTheme {
KeyInvalidatedWarningSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun KeyInvalidatedWarningPreview_Dark() {
TangemTheme(isDark = true) {
KeyInvalidatedWarningSample()
}
}
// endregion Preview

View file

@ -11,9 +11,15 @@ internal sealed interface DialogModel {
val onConfirm: () -> Unit,
val onDismiss: () -> Unit,
) : DialogModel
}
data class BiometricsLockoutDialog(
internal sealed interface WarningModel : DialogModel {
data class BiometricsLockoutWarning(
val isPermanent: Boolean,
val onDismiss: () -> Unit,
) : DialogModel
) : WarningModel
data class KeyInvalidatedWarning(
val onDismiss: () -> Unit,
) : WarningModel
}

View file

@ -27,6 +27,7 @@ import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
internal class WelcomeMiddleware {
val middleware: Middleware<AppState> = { _, appStateProvider ->
@ -69,16 +70,15 @@ internal class WelcomeMiddleware {
scope.launch {
userWalletsListManager.unlockIfLockable()
.doOnFailure { error ->
Timber.e(error, "Unable to unlock user wallets with biometrics")
store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Error(error))
}
.doOnSuccess { selectedUserWallet ->
if (selectedUserWallet != null) {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Success)
store.onUserWalletSelected(selectedUserWallet)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Success)
store.onUserWalletSelected(selectedUserWallet)
intentHandler.handleWalletConnectLink(state.intent)
}
intentHandler.handleWalletConnectLink(state.intent)
}
}
}
@ -89,6 +89,7 @@ internal class WelcomeMiddleware {
userWalletsListManager.save(userWallet, canOverride = true)
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Error(error))
}
.doOnSuccess {

View file

@ -24,7 +24,7 @@ import com.tangem.core.ui.fragments.ComposeFragment
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
import com.tangem.tap.features.welcome.redux.WelcomeAction
import com.tangem.tap.features.welcome.ui.components.BiometricsLockoutDialog
import com.tangem.tap.features.welcome.ui.components.WarningDialog
import com.tangem.tap.features.welcome.ui.components.WelcomeScreenContent
import com.tangem.tap.store
import com.tangem.wallet.R
@ -49,7 +49,7 @@ internal class WelcomeFragment : ComposeFragment<WelcomeScreenState>() {
) {
val snackbarHostState = remember { SnackbarHostState() }
val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference())
val biometricsLockoutDialog by rememberUpdatedState(newValue = state.biometricsLockoutDialog)
val warning by rememberUpdatedState(newValue = state.warning)
val backgroundColor = colorResource(id = R.color.background_primary)
SystemBarsEffect {
@ -80,7 +80,7 @@ internal class WelcomeFragment : ComposeFragment<WelcomeScreenState>() {
)
}
BiometricsLockoutDialog(biometricsLockoutDialog)
WarningDialog(warning)
LaunchedEffect(key1 = errorMessage) {
errorMessage?.let {

View file

@ -2,12 +2,12 @@ package com.tangem.tap.features.welcome.ui
import androidx.compose.runtime.Immutable
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.welcome.ui.model.BiometricsLockoutDialog
import com.tangem.tap.features.welcome.ui.model.WarningModel
@Immutable
internal data class WelcomeScreenState(
val showUnlockWithBiometricsProgress: Boolean = false,
val showUnlockWithCardProgress: Boolean = false,
val biometricsLockoutDialog: BiometricsLockoutDialog? = null,
val warning: WarningModel? = null,
val error: TextReference? = null,
)

View file

@ -2,14 +2,14 @@ package com.tangem.tap.features.welcome.ui
import androidx.lifecycle.ViewModel
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.SignIn
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.userWalletList.UserWalletsListError
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.welcome.redux.WelcomeAction
import com.tangem.tap.features.welcome.redux.WelcomeState
import com.tangem.tap.features.welcome.ui.model.BiometricsLockoutDialog
import com.tangem.tap.features.welcome.ui.model.WarningModel
import com.tangem.tap.store
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -41,15 +41,15 @@ internal class WelcomeViewModel : ViewModel(), StoreSubscriber<WelcomeState> {
// TODO: Refactor errors handling
override fun newState(state: WelcomeState) {
val biometricsLockoutDialog = createBiometricLockoutDialogIfNeeded(state.error)
val warning = createWarningIfNeeded(state.error)
stateInternal.update { prevState ->
prevState.copy(
showUnlockWithBiometricsProgress = state.isUnlockWithBiometricsInProgress,
showUnlockWithCardProgress = state.isUnlockWithCardInProgress,
biometricsLockoutDialog = biometricsLockoutDialog,
warning = warning,
error = state.error
?.takeIf { !it.silent && biometricsLockoutDialog == null }
?.takeIf { !it.silent && warning == null }
?.let { e ->
e.messageResId?.let { TextReference.Res(it) }
?: TextReference.Str(e.customMessage)
@ -62,24 +62,23 @@ internal class WelcomeViewModel : ViewModel(), StoreSubscriber<WelcomeState> {
store.unsubscribe(this)
}
private fun createBiometricLockoutDialogIfNeeded(error: TangemError?): BiometricsLockoutDialog? {
private fun createWarningIfNeeded(error: TangemError?): WarningModel? {
return when (error) {
is TangemSdkError.BiometricsAuthenticationLockout -> BiometricsLockoutDialog(
isPermanent = false,
onDismiss = this::dismissDialog,
is UserWalletsListError.BiometricsAuthenticationLockout -> WarningModel.BiometricsLockoutWarning(
isPermanent = error.isPermanent,
onDismiss = this::dismissWarning,
)
is TangemSdkError.BiometricsAuthenticationPermanentLockout -> BiometricsLockoutDialog(
isPermanent = true,
onDismiss = this::dismissDialog,
is UserWalletsListError.InvalidEncryptionKey -> WarningModel.KeyInvalidatedWarning(
onDismiss = this::dismissWarning,
)
else -> null
}
}
private fun dismissDialog() {
private fun dismissWarning() {
stateInternal.update { prevState ->
prevState.copy(
biometricsLockoutDialog = null,
warning = null,
)
}
closeError()

View file

@ -1,96 +0,0 @@
package com.tangem.tap.features.welcome.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.welcome.ui.model.BiometricsLockoutDialog
import com.tangem.wallet.R
@Composable
internal fun BiometricsLockoutDialog(
dialog: BiometricsLockoutDialog?,
) {
if (dialog == null) return
BasicDialog(
title = stringResource(id = R.string.biometric_lockout_warning_title),
message = stringResource(
id = if (dialog.isPermanent) {
R.string.biometric_lockout_permanent_warning_description
} else {
R.string.biometric_lockout_warning_description
},
),
onDismissDialog = dialog.onDismiss,
confirmButton = DialogButton(
title = stringResource(id = R.string.common_ok),
onClick = dialog.onDismiss,
),
)
}
// region Preview
@Composable
private fun BiometricsLockoutDialogSample(
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
BiometricsLockoutDialog(
dialog = BiometricsLockoutDialog(
isPermanent = false,
onDismiss = {},
),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun BiometricsLockoutDialogPreview_Light() {
TangemTheme {
BiometricsLockoutDialogSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun BiometricsLockoutDialogPreview_Dark() {
TangemTheme(isDark = true) {
BiometricsLockoutDialogSample()
}
}
@Composable
private fun BiometricsLockoutDialog_Permanent_Sample(
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
BiometricsLockoutDialog(
dialog = BiometricsLockoutDialog(
isPermanent = true,
onDismiss = {},
),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun BiometricsLockoutDialog_Permanent_Preview_Light() {
TangemTheme {
BiometricsLockoutDialog_Permanent_Sample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun BiometricsLockoutDialog_Permanent_Preview_Dark() {
TangemTheme(isDark = true) {
BiometricsLockoutDialog_Permanent_Sample()
}
}
// endregion Preview

View file

@ -0,0 +1,138 @@
package com.tangem.tap.features.welcome.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.welcome.ui.model.WarningModel
import com.tangem.wallet.R
@Composable
internal fun WarningDialog(
warning: WarningModel?,
) {
when (warning) {
null -> Unit
is WarningModel.BiometricsLockoutWarning -> {
BasicDialog(
title = stringResource(id = R.string.biometric_lockout_warning_title),
message = stringResource(
id = if (warning.isPermanent) {
R.string.biometric_lockout_permanent_warning_description
} else {
R.string.biometric_lockout_warning_description
},
),
onDismissDialog = warning.onDismiss,
confirmButton = DialogButton(
title = stringResource(id = R.string.common_ok),
onClick = warning.onDismiss,
),
)
}
is WarningModel.KeyInvalidatedWarning -> {
BasicDialog(
title = stringResource(id = R.string.common_attention),
message = stringResource(id = R.string.key_invalidated_warning_description),
onDismissDialog = warning.onDismiss,
confirmButton = DialogButton(
title = stringResource(id = R.string.common_ok),
onClick = warning.onDismiss,
),
)
}
}
}
// region Preview
@Composable
private fun BiometricsLockoutDialogSample(
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
WarningDialog(
warning = WarningModel.BiometricsLockoutWarning(
isPermanent = false,
onDismiss = {},
),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun BiometricsLockoutDialogPreview_Light() {
TangemTheme {
BiometricsLockoutDialogSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun BiometricsLockoutDialogPreview_Dark() {
TangemTheme(isDark = true) {
BiometricsLockoutDialogSample()
}
}
@Composable
private fun BiometricsLockoutDialog_Permanent_Sample(
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
WarningDialog(
warning = WarningModel.BiometricsLockoutWarning(
isPermanent = true,
onDismiss = {},
),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun BiometricsLockoutDialog_Permanent_Preview_Light() {
TangemTheme {
BiometricsLockoutDialog_Permanent_Sample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun BiometricsLockoutDialog_Permanent_Preview_Dark() {
TangemTheme(isDark = true) {
BiometricsLockoutDialog_Permanent_Sample()
}
}
// region Preview
@Composable
private fun KeyInvalidatedWarningSample(
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
WarningDialog(warning = WarningModel.KeyInvalidatedWarning(onDismiss = {}))
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun KeyInvalidatedWarningPreview_Light() {
TangemTheme {
KeyInvalidatedWarningSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun KeyInvalidatedWarningPreview_Dark() {
TangemTheme(isDark = true) {
KeyInvalidatedWarningSample()
}
}
// endregion Preview
// endregion Preview

View file

@ -1,6 +0,0 @@
package com.tangem.tap.features.welcome.ui.model
internal data class BiometricsLockoutDialog(
val isPermanent: Boolean,
val onDismiss: () -> Unit,
)

View file

@ -0,0 +1,12 @@
package com.tangem.tap.features.welcome.ui.model
internal sealed interface WarningModel {
data class BiometricsLockoutWarning(
val isPermanent: Boolean,
val onDismiss: () -> Unit,
) : WarningModel
data class KeyInvalidatedWarning(
val onDismiss: () -> Unit,
) : WarningModel
}