Updated on 2026-08-14

This commit is contained in:
Tangem 2023-02-14 14:37:02 +03:00
commit c82fadbf63
17 changed files with 321 additions and 88 deletions

View file

@ -114,6 +114,9 @@ object TangemSdkErrorMapper {
is TangemSdkError.BackupFailedIncompatibleBatch -> TangemSdkError.BackupFailedIncompatibleBatch()
is TangemSdkError.BiometricsUnavailable -> error
is TangemSdkError.BiometricsAuthenticationFailed -> error
is TangemSdkError.BiometricsAuthenticationLockout -> error
is TangemSdkError.BiometricsAuthenticationPermanentLockout -> error
is TangemSdkError.UserCanceledBiometricsAuthentication -> error
}
}
}

View file

@ -13,28 +13,4 @@ sealed class UserWalletListError(code: Int) : TangemError(code) {
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
}
class SaveEncryptionKeysError(
override val cause: Throwable,
) : UserWalletListError(code = 60001) {
override var customMessage: String = "Encryption keys could not be saved: ${cause.localizedMessage}"
}
class ReceiveEncryptionKeysError(
override val cause: Throwable,
) : UserWalletListError(code = 60002) {
override var customMessage: String = "Encryption keys could not be received: ${cause.localizedMessage}"
}
class SaveSensitiveInformationError(
override val cause: Throwable,
) : UserWalletListError(code = 60003) {
override var customMessage: String = "Sensitive information could not be saved: ${cause.localizedMessage}"
}
class ReceiveSensitiveInformationError(
override val cause: Throwable,
) : UserWalletListError(code = 60004) {
override var customMessage: String = "Sensitive information could not be received: ${cause.localizedMessage}"
}
}

View file

@ -10,10 +10,8 @@ import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
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.UserWalletListError
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository
import kotlinx.coroutines.Dispatchers
@ -39,18 +37,12 @@ internal class BiometricUserWalletsKeysRepository(
override suspend fun getAll(): CompletionResult<List<UserWalletEncryptionKey>> {
return withContext(Dispatchers.IO) {
getAllInternal()
.mapFailure { error ->
UserWalletListError.ReceiveEncryptionKeysError(error.cause ?: error)
}
}
}
override suspend fun save(encryptionKey: UserWalletEncryptionKey): CompletionResult<Unit> {
return withContext(Dispatchers.IO) {
storeEncryptionKey(encryptionKey)
.mapFailure { error ->
UserWalletListError.SaveEncryptionKeysError(error.cause ?: error)
}
}
}
@ -91,7 +83,7 @@ internal class BiometricUserWalletsKeysRepository(
getEncryptionKey(userWalletId)
.doOnFailure { error ->
// If the user cancels biometric authentication, cancel the request for all keys
if (error is TangemSdkError.BiometricsAuthenticationFailed) {
if (error is TangemSdkError.UserCanceledBiometricsAuthentication) {
return CompletionResult.Failure(error)
}
}

View file

@ -6,12 +6,10 @@ import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.common.CompletionResult
import com.tangem.common.catching
import com.tangem.common.mapFailure
import com.tangem.common.services.secure.SecureStorage
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.common.extensions.filterNotNull
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.userWalletList.UserWalletListError
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
import com.tangem.tap.domain.userWalletList.repository.UserWalletsSensitiveInformationRepository
@ -48,9 +46,6 @@ internal class DefaultUserWalletsSensitiveInformationRepository(
.apply { set(userWallet.walletId.stringValue, encryptedSensitiveInformation) }
.let { saveInternal(it) }
}
.mapFailure { error ->
UserWalletListError.SaveSensitiveInformationError(error.cause ?: error)
}
}
override suspend fun getAll(
@ -72,9 +67,6 @@ internal class DefaultUserWalletsSensitiveInformationRepository(
}
.filterNotNull()
}
.mapFailure { error ->
UserWalletListError.ReceiveSensitiveInformationError(error.cause ?: error)
}
}
override suspend fun delete(userWalletsIds: List<UserWalletId>): CompletionResult<Unit> {

View file

@ -75,8 +75,11 @@ data class WalletState(
get() = walletsStores.mapNotNull { it.walletManager }
private val primaryWalletStore: WalletStore?
get() = if (isMultiwalletAllowed || walletsStores.isEmpty() || walletsStores.size > 1) null
else walletsStores[0]
get() = if (isMultiwalletAllowed || walletsStores.isEmpty() || walletsStores.size > 1) {
null
} else {
walletsStores[0]
}
private val primaryWalletManager: WalletManager?
get() = primaryWalletStore?.walletManager

View file

@ -131,7 +131,8 @@ class OnWalletLoadedReducer {
}
val walletData = walletState.primaryWalletData?.copy(
currencyData = BalanceWidgetData(
balanceStatus, wallet.blockchain.fullName,
status = balanceStatus,
currency = wallet.blockchain.fullName,
currencySymbol = wallet.blockchain.currency,
blockchainAmount = amount,
amount = amount,

View file

@ -3,50 +3,13 @@ package com.tangem.tap.features.walletSelector.ui
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.toFormattedFiatValue
import com.tangem.tap.domain.model.TotalFiatBalance
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.walletSelector.redux.UserWalletModel
import com.tangem.tap.features.walletSelector.redux.WalletSelectorState
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.UserWalletItem
import java.math.BigDecimal
internal fun WalletSelectorScreenState.updateWithNewState(
newState: WalletSelectorState,
): WalletSelectorScreenState {
val walletsUi = newState.wallets.toUiModels(newState.fiatCurrency)
val walletsIds = walletsUi.map { it.id }
val multiCurrencyWallets = arrayListOf<MultiCurrencyUserWalletItem>()
val singleCurrencyWallets = arrayListOf<SingleCurrencyUserWalletItem>()
walletsUi.forEach { wallet ->
when (wallet) {
is MultiCurrencyUserWalletItem -> {
multiCurrencyWallets.add(wallet)
}
is SingleCurrencyUserWalletItem -> {
singleCurrencyWallets.add(wallet)
}
}
}
return this.copy(
multiCurrencyWallets = multiCurrencyWallets,
singleCurrencyWallets = singleCurrencyWallets,
selectedUserWalletId = newState.selectedWalletId,
editingUserWalletsIds = editingUserWalletsIds.filter { it in walletsIds },
isLocked = newState.isLocked,
showUnlockProgress = newState.isUnlockInProgress,
showAddCardProgress = newState.isCardSavingInProgress,
error = newState.error
?.takeUnless { it.silent }
?.let { error ->
error.messageResId?.let { TextReference.Res(it) }
?: TextReference.Str(error.customMessage)
},
)
}
private fun List<UserWalletModel>.toUiModels(
internal fun List<UserWalletModel>.toUiModels(
appCurrency: FiatCurrency,
): Sequence<UserWalletItem> {
return this.asSequence().map { userWalletModel ->

View file

@ -25,6 +25,7 @@ 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.RemoveWalletDialogContent
import com.tangem.tap.features.walletSelector.ui.components.RenameWalletDialogContent
import com.tangem.tap.features.walletSelector.ui.components.WalletSelectorScreenContent
@ -89,6 +90,7 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment<Wa
when (dialog) {
is DialogModel.RemoveWalletDialog -> RemoveWalletDialogContent(dialog)
is DialogModel.RenameWalletDialog -> RenameWalletDialogContent(dialog)
is DialogModel.BiometricsLockoutDialog -> BiometricsLockoutDialogContent(dialog)
}
}
}

View file

@ -2,13 +2,18 @@ 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.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction
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.store
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletStoresManager
@ -136,9 +141,41 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
store.dispatch(WalletSelectorAction.CloseError)
}
// TODO: Refactor errors handling
override fun newState(state: WalletSelectorState) {
stateInternal.update { prevState ->
prevState.updateWithNewState(state)
val walletsUi = state.wallets.toUiModels(state.fiatCurrency)
val walletsIds = walletsUi.map { it.id }
val multiCurrencyWallets = arrayListOf<MultiCurrencyUserWalletItem>()
val singleCurrencyWallets = arrayListOf<SingleCurrencyUserWalletItem>()
walletsUi.forEach { wallet ->
when (wallet) {
is MultiCurrencyUserWalletItem -> {
multiCurrencyWallets.add(wallet)
}
is SingleCurrencyUserWalletItem -> {
singleCurrencyWallets.add(wallet)
}
}
}
val biometricsLockoutDialog = createBiometricsLockoutDialogIfNeeded(state.error, prevState.dialog)
prevState.copy(
multiCurrencyWallets = multiCurrencyWallets,
singleCurrencyWallets = singleCurrencyWallets,
selectedUserWalletId = state.selectedWalletId,
editingUserWalletsIds = prevState.editingUserWalletsIds.filter { it in walletsIds },
isLocked = state.isLocked,
showUnlockProgress = state.isUnlockInProgress,
showAddCardProgress = state.isCardSavingInProgress,
dialog = biometricsLockoutDialog,
error = state.error
?.takeIf { !it.silent && biometricsLockoutDialog == null }
?.let { error ->
error.messageResId?.let { TextReference.Res(it) }
?: TextReference.Str(error.customMessage)
},
)
}
}
@ -146,6 +183,32 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
store.unsubscribe(this)
}
private fun createBiometricsLockoutDialogIfNeeded(
error: TangemError?,
currentDialogModel: DialogModel?,
): DialogModel? {
return when (error) {
is TangemSdkError.BiometricsAuthenticationLockout -> DialogModel.BiometricsLockoutDialog(
isPermanent = false,
onDismiss = this::dismissDialog,
)
is TangemSdkError.BiometricsAuthenticationPermanentLockout -> DialogModel.BiometricsLockoutDialog(
isPermanent = true,
onDismiss = this::dismissDialog,
)
else -> currentDialogModel
}
}
private fun dismissDialog() {
stateInternal.update { prevState ->
prevState.copy(
dialog = null,
)
}
closeError()
}
private fun editWallet(userWalletId: UserWalletId) {
stateInternal.update { prevState ->
prevState.copy(

View file

@ -0,0 +1,95 @@
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.DialogModel
import com.tangem.wallet.R
@Composable
internal fun BiometricsLockoutDialogContent(
dialog: DialogModel.BiometricsLockoutDialog,
) {
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) {
BiometricsLockoutDialogContent(
dialog = DialogModel.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) {
BiometricsLockoutDialogContent(
dialog = DialogModel.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

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

View file

@ -25,6 +25,7 @@ import com.tangem.core.ui.fragments.ComposeFragment
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.analytics.events.SignIn
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
import com.tangem.tap.features.welcome.ui.components.BiometricsLockoutDialog
import com.tangem.tap.features.welcome.ui.components.WelcomeScreenContent
import com.tangem.wallet.R
@ -45,6 +46,7 @@ internal class WelcomeFragment : ComposeFragment<WelcomeScreenState>() {
override fun ScreenContent(state: WelcomeScreenState, modifier: Modifier) {
val snackbarHostState = remember { SnackbarHostState() }
val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference())
val biometricsLockoutDialog by rememberUpdatedState(newValue = state.biometricsLockoutDialog)
val backgroundColor = colorResource(id = R.color.background_primary)
SystemBarsEffect {
@ -75,6 +77,8 @@ internal class WelcomeFragment : ComposeFragment<WelcomeScreenState>() {
)
}
BiometricsLockoutDialog(biometricsLockoutDialog)
LaunchedEffect(key1 = errorMessage) {
errorMessage?.let {
snackbarHostState.showSnackbar(it)

View file

@ -2,10 +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
@Immutable
internal data class WelcomeScreenState(
val showUnlockWithBiometricsProgress: Boolean = false,
val showUnlockWithCardProgress: Boolean = false,
val biometricsLockoutDialog: BiometricsLockoutDialog? = null,
val error: TextReference? = null,
)

View file

@ -1,12 +1,15 @@
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.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.store
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -36,16 +39,20 @@ internal class WelcomeViewModel : ViewModel(), StoreSubscriber<WelcomeState> {
store.dispatch(WelcomeAction.CloseError)
}
// TODO: Refactor errors handling
override fun newState(state: WelcomeState) {
val biometricsLockoutDialog = createBiometricLockoutDialogIfNeeded(state.error)
stateInternal.update { prevState ->
prevState.copy(
showUnlockWithBiometricsProgress = state.isUnlockWithBiometricsInProgress,
showUnlockWithCardProgress = state.isUnlockWithCardInProgress,
biometricsLockoutDialog = biometricsLockoutDialog,
error = state.error
?.takeUnless { it.silent }
?.let { error ->
error.messageResId?.let { TextReference.Res(it) }
?: TextReference.Str(error.customMessage)
?.takeIf { !it.silent && biometricsLockoutDialog == null }
?.let { e ->
e.messageResId?.let { TextReference.Res(it) }
?: TextReference.Str(e.customMessage)
},
)
}
@ -55,6 +62,29 @@ internal class WelcomeViewModel : ViewModel(), StoreSubscriber<WelcomeState> {
store.unsubscribe(this)
}
private fun createBiometricLockoutDialogIfNeeded(error: TangemError?): BiometricsLockoutDialog? {
return when (error) {
is TangemSdkError.BiometricsAuthenticationLockout -> BiometricsLockoutDialog(
isPermanent = false,
onDismiss = this::dismissDialog,
)
is TangemSdkError.BiometricsAuthenticationPermanentLockout -> BiometricsLockoutDialog(
isPermanent = true,
onDismiss = this::dismissDialog,
)
else -> null
}
}
private fun dismissDialog() {
stateInternal.update { prevState ->
prevState.copy(
biometricsLockoutDialog = null,
)
}
closeError()
}
private fun subscribeToStoreChanges() {
store.subscribe(this) { appState ->
appState.skip { old, new -> old.welcomeState == new.welcomeState }

View file

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

View file

@ -64,7 +64,7 @@ object Versions {
const val tangemBlockchainSdk = "develop-155"
// const val tangemBlockchainSdk = "0.0.1" // Keep it! - used for local builds
const val tangemCardSdk = "develop-190"
const val tangemCardSdk = "develop-191"
// const val tangemCardSgk = "0.0.1" // Keep it! - used for local builds
// endregion Tangem