Updated on 2026-08-14
This commit is contained in:
parent
c2aef02443
commit
605d7da00d
28 changed files with 415 additions and 205 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
),
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
package com.tangem.tap.features.welcome.ui.model
|
||||
|
||||
internal data class BiometricsLockoutDialog(
|
||||
val isPermanent: Boolean,
|
||||
val onDismiss: () -> Unit,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue