diff --git a/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkErrorMapper.kt b/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkErrorMapper.kt index b6fb925fd4..20e273fda2 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkErrorMapper.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkErrorMapper.kt @@ -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 } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletListError.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletListError.kt deleted file mode 100644 index bf42b9c005..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletListError.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListError.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListError.kt new file mode 100644 index 0000000000..03ec981f62 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListError.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt index ea9b4c6bb9..4286bfd8c0 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt @@ -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 + suspend fun unlock(): CompletionResult /** * Remove [UserWallet]s from [userWallets] and set [isLocked] as true diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManagerExtensions.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManagerExtensions.kt index 6da40c3b93..b795c7df78 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManagerExtensions.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManagerExtensions.kt @@ -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 get() = asLockable()?.isLocked ?: flowOf(false) @@ -24,6 +26,8 @@ val UserWalletsListManager.isLocked: Flow * 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 { - return asLockable()?.unlock() ?: CompletionResult.Success(selectedUserWalletSync) +suspend fun UserWalletsListManager.unlockIfLockable(): CompletionResult { + 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() diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt index 060464fd2f..b4bcf48c73 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt @@ -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 { + override suspend fun unlock(): CompletionResult { + // 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 = 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 = emptyList(), val selectedUserWalletId: UserWalletId? = null, val isLocked: Boolean = true, + val hasLockedUserWalletsAfterUnlock: Boolean = false, ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt index 01a31ebff4..f726dba792 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt @@ -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) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt index f3f39a647f..0307b8b7d4 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt @@ -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> { 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 { - return biometricStorage.get(StorageKey.WalletEncryptionKey(userWalletId).name) + return biometricStorage.get(StorageKey.UserWalletEncryptionKey(userWalletId).name) .map { it.decodeToKey() } } private suspend fun storeEncryptionKey(encryptionKey: UserWalletEncryptionKey): CompletionResult { 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 { - return biometricStorage.delete(StorageKey.WalletEncryptionKey(userWalletId).name) + return biometricStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name) } private suspend fun getUserWalletsIds(): List { @@ -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}" } diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt index 11fec17c6d..dbb367554c 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt @@ -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() { private val viewModel by viewModels() @@ -93,7 +95,8 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment RemoveWalletDialogContent(dialog) is DialogModel.RenameWalletDialog -> RenameWalletDialogContent(dialog) - is DialogModel.BiometricsLockoutDialog -> BiometricsLockoutDialogContent(dialog) + is WarningModel.BiometricsLockoutWarning -> BiometricsLockoutWarningContent(dialog) + is WarningModel.KeyInvalidatedWarning -> KeyInvalidatedWarningContent(dialog) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt index b048098525..9384a5f22b 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt @@ -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 error.messageResId?.let { TextReference.Res(it) } ?: TextReference.Str(error.customMessage) @@ -184,24 +185,23 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber 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, diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/BiometricsLockoutDialogContent.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/BiometricsLockoutWarningContent.kt similarity index 81% rename from app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/BiometricsLockoutDialogContent.kt rename to app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/BiometricsLockoutWarningContent.kt index cb724fe222..1416b5af61 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/BiometricsLockoutDialogContent.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/BiometricsLockoutWarningContent.kt @@ -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 = {}, ), diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/KeyInvalidatedWarningContent.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/KeyInvalidatedWarningContent.kt new file mode 100644 index 0000000000..0b31a73101 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/KeyInvalidatedWarningContent.kt @@ -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 \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/DialogModel.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/DialogModel.kt index 309df35752..8024e00021 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/DialogModel.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/DialogModel.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index c8cde60e79..84ff91eebf 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -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 = { _, 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 { diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt index b80d65ce7a..e346adb6d1 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt @@ -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() { ) { 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() { ) } - BiometricsLockoutDialog(biometricsLockoutDialog) + WarningDialog(warning) LaunchedEffect(key1 = errorMessage) { errorMessage?.let { diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt index f9ea592c62..701042816e 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt @@ -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, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt index 042034cfd8..4bbcdf76ea 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt @@ -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 { // 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 { 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() diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/BiometricsLockoutDialog.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/BiometricsLockoutDialog.kt deleted file mode 100644 index 0a594b864d..0000000000 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/BiometricsLockoutDialog.kt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt new file mode 100644 index 0000000000..fb974cc393 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt @@ -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 \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/model/BiometricsLockoutDialog.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/model/BiometricsLockoutDialog.kt deleted file mode 100644 index bb1e4bf3cd..0000000000 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/model/BiometricsLockoutDialog.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.tap.features.welcome.ui.model - -internal data class BiometricsLockoutDialog( - val isPermanent: Boolean, - val onDismiss: () -> Unit, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/model/WarningModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/model/WarningModel.kt new file mode 100644 index 0000000000..4940a4642a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/model/WarningModel.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt index a983ea5687..72836cacca 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -28,7 +28,11 @@ class UserWalletManagerImpl( private val appStateHolder: AppStateHolder, ) : UserWalletManager { - override suspend fun getUserTokens(networkId: String, isExcludeCustom: Boolean): List { + override suspend fun getUserTokens( + networkId: String, + derivationPath: String?, + isExcludeCustom: Boolean, + ): List { val card = appStateHolder.getActualCard() val userTokensRepository = requireNotNull(appStateHolder.userTokensRepository) { "userTokensRepository is null" } @@ -40,7 +44,9 @@ class UserWalletManagerImpl( } else { true } - it.blockchain.toNetworkId() == networkId && checkCustom + it.blockchain.toNetworkId() == networkId && + checkCustom && + it.derivationPath == derivationPath } .map { if (it is com.tangem.tap.features.wallet.models.Currency.Token) { diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt index b02e115cd7..c59727ad73 100644 --- a/buildSrc/src/main/java/Versions.kt +++ b/buildSrc/src/main/java/Versions.kt @@ -64,7 +64,7 @@ object Versions { const val tangemBlockchainSdk = "develop-174" // const val tangemBlockchainSdk = "0.0.1" // Keep it! - used for local builds - const val tangemCardSdk = "develop-199" + const val tangemCardSdk = "develop-200" // const val tangemCardSdk = "0.0.1" // Keep it! - used for local builds // endregion Tangem diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index ffa90384f5..a0dd22a168 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -339,7 +339,7 @@ Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds Not enough funds for fee in your %1$s wallet to create a transaction. Top up your %2$s wallet first. - Transaction in progress... + Transaction in progress… Waiting Approve Give Permission @@ -371,7 +371,7 @@ You don\'t have any transactions yet Failed to load transactions Transactions - In progress... + In progress… You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d This one that you are holding in your hands and the other one with number %s.\n\nBoth cards can be used to extract funds from this wallet. One wallet. Two cards. @@ -384,6 +384,7 @@ Add new wallet Are you sure you want to delete this wallet? %d selected + An error has occurred, please scan your card to log in This wallet has already been saved, you can add another one Multi-currency Wallet name diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index a9c847c30f..43efbd281e 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -339,7 +339,7 @@ Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds Not enough funds for fee in your %1$s wallet to create a transaction. Top up your %2$s wallet first. - Transaction in progress... + Transaction in progress… Waiting Approve Give Permission @@ -371,7 +371,7 @@ You don\'t have any transactions yet Failed to load transactions Transactions - In progress... + In progress… You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d This one that you are holding in your hands and the other one with number %s.\n\nBoth cards can be used to extract funds from this wallet. One wallet. Two cards. @@ -384,6 +384,7 @@ Add new wallet Are you sure you want to delete this wallet? %d selected + An error has occurred, please scan your card to log in This wallet has already been saved, you can add another one Multi-currency Wallet name diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 4ae4f65caf..b1209fc917 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -339,7 +339,7 @@ Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds Not enough funds for fee in your %1$s wallet to create a transaction. Top up your %2$s wallet first. - Transaction in progress... + Transaction in progress… Waiting Approve Give Permission @@ -371,7 +371,7 @@ You don\'t have any transactions yet Failed to load transactions Transactions - In progress... + In progress… You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d This one that you are holding in your hands and the other one with number %s.\n\nBoth cards can be used to extract funds from this wallet. One wallet. Two cards. @@ -384,6 +384,7 @@ Add new wallet Are you sure you want to delete this wallet? %d selected + An error has occurred, please scan your card to log in This wallet has already been saved, you can add another one Multi-currency Wallet name diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 86620877b5..41175e518c 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -132,6 +132,7 @@ Нажмите, чтобы подписать Приложите карту Внутренняя ошибка: не удается найти менеджер кошельков + Вы обновили данные биометрии, отсканируйте свою карту для входа Управление токенами Чтобы защитить свои активы, мы советуем вам выполнить эту процедуру Бэкап кошелька не был произведен @@ -384,6 +385,7 @@ Добавить новый кошелек Вы уверены, что хотите удалить этот кошелек? %d выбрано + Произошла ошибка, пожалуйста, отсканируйте свою карту для входа Этот кошелек уже был сохранен, вы можете добавить другой Мультивалютные Имя кошелька @@ -418,6 +420,7 @@ Не удалось установить сессию WalletConnect за отведённое время. Пожалуйста, повторите попытку позже. Запрос на подключение через WalletConnect содержит неподдерживаемые блокчеины. Неподдерживаемые блокчеины:\n Cоединение с этим Dapp сервисом не может быть установлено из-за его технической реализации. + Неверная карта выбрана в приложении Tangem Произошла непредвиденная ошибка. Код ошибки: %d Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки. Сеть %s не найдена. Пожалуйста, добавьте её и попробуйте заново. Нет открытых сессий WalletConnect diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index 96b180a83f..dcc7b86b54 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -339,7 +339,7 @@ Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. 餘額不足 您的 %1$s 錢包中沒有足夠的資金來創建交易。首先為您的 %2$s 錢包充值 - 交易進行中... + 交易進行中… 等待中 允許 賦予權限 @@ -371,7 +371,7 @@ 您還沒有任何交易 無法加載交易 交易 - 進行中... + 進行中… 您掃描了同一張卡片。要創建雙錢包,您需要掃描編號為 %d 的卡 這一個是你手裡拿著的,另一個是編號為 %s 的,這兩張卡都可以用來從這個錢包中提取資金 一個錢包,兩張卡片 @@ -384,6 +384,7 @@ 添加新錢包 您確定要刪除此錢包? 已選擇 %d + An error has occurred, please scan your card to log in 此錢包已保存,您可以再添加一個 多幣種 錢包名稱 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index afa7b78109..7425f3b247 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -132,6 +132,7 @@ Tap to sign Tap the card Internal error: wallet manager not found + You have updated biometrics, scan your card to enter Manage tokens To protect your assets, we advise you to carry out this procedure Your wallet has not been backed up @@ -371,7 +372,7 @@ You don\'t have any transactions yet Failed to load transactions Transactions - In progress... + In progress… You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d This one that you are holding in your hands and the other one with number %s.\n\nBoth cards can be used to extract funds from this wallet. One wallet. Two cards. @@ -384,6 +385,7 @@ Add new wallet Are you sure you want to delete this wallet? %d selected + An error has occurred, please scan your card to log in This wallet has already been saved, you can add another one Multi-currency Wallet name @@ -418,6 +420,7 @@ Failed to establish WalletConnect session: timeout error. Please, try again later. Session request contains unsupported blockchains for WalletConnect connection. Unsupported blockchains:\n Connection with this Dapp cannot be established due to its technical implementation. + Wrong card selected in Tangem App We\'ve encountered unknown error. Error code: %d. If the problem persists — feel free to contact our support %s network not found. Please, add it first and try again. No opened WalletConnect sessions diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 461f048ca8..0c74003585 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -94,9 +94,10 @@ interface SwapInteractor { /** * Returns token in wallet balance * + * @param networkId * @param token */ - fun getTokenBalance(token: Currency): SwapAmount + fun getTokenBalance(networkId: String, token: Currency): SwapAmount fun isAvailableToSwap(networkId: String): Boolean } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 98350f3244..f1ef395ed1 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -56,14 +56,18 @@ internal class SwapInteractorImpl @Inject constructor( // replace tokens in wallet tokens list with loaded same val loadedOnWalletsMap = mutableSetOf() - val tokensInWallet = userWalletManager.getUserTokens(networkId = networkId, isExcludeCustom = true) - .filter { it.symbol != initialCurrency.symbol } - .map { token -> - allLoadedTokens.firstOrNull { it.symbol == token.symbol }?.let { - loadedOnWalletsMap.add(it.symbol) - it - } ?: cryptoCurrencyConverter.convertBack(token) - } + val tokensInWallet = + userWalletManager.getUserTokens( + networkId = networkId, + derivationPath = derivationPath, + isExcludeCustom = true, + ).filter { it.symbol != initialCurrency.symbol } + .map { token -> + allLoadedTokens.firstOrNull { it.symbol == token.symbol }?.let { + loadedOnWalletsMap.add(it.symbol) + it + } ?: cryptoCurrencyConverter.convertBack(token) + } val loadedTokens = allLoadedTokens .filter { !loadedOnWalletsMap.contains(it.symbol) @@ -72,7 +76,7 @@ internal class SwapInteractorImpl @Inject constructor( .mapValues { SwapAmount(it.value.value, it.value.decimals) } val appCurrency = userWalletManager.getUserAppCurrency() val rates = repository.getRates(appCurrency.code, tokensInWallet.map { it.id }) - cache.cacheBalances(tokensBalance) + cache.cacheBalances(networkId, derivationPath, tokensBalance) cache.cacheLoadedTokens(loadedTokens.map { TokenWithBalance(it) }) cache.cacheInWalletTokens(getTokensWithBalance(tokensInWallet, tokensBalance, rates, appCurrency)) return TokensDataState( @@ -147,7 +151,7 @@ internal class SwapInteractorImpl @Inject constructor( syncWalletBalanceForTokens(networkId, listOf(fromToken, toToken)) val amountDecimal = toBigDecimalOrNull(amountToSwap) if (amountDecimal == null || amountDecimal.compareTo(BigDecimal.ZERO) == 0) { - return createEmptyAmountState(fromToken, toToken) + return createEmptyAmountState(networkId, fromToken, toToken) } val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken)) val fromTokenAddress = getTokenAddress(fromToken) @@ -157,7 +161,7 @@ internal class SwapInteractorImpl @Inject constructor( allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) transactionManager.updateWalletManager(networkId, derivationPath) } - val isBalanceWithoutFeeEnough = isBalanceEnough(fromToken, amount, null) + val isBalanceWithoutFeeEnough = isBalanceEnough(networkId, fromToken, amount, null) return if (isAllowedToSpend && isBalanceWithoutFeeEnough) { loadSwapData( networkId = networkId, @@ -224,8 +228,12 @@ internal class SwapInteractorImpl @Inject constructor( } } - override fun getTokenBalance(token: Currency): SwapAmount { - return cache.getBalanceForToken(token.symbol) ?: SwapAmount(BigDecimal.ZERO, getTokenDecimals(token)) + override fun getTokenBalance(networkId: String, token: Currency): SwapAmount { + return cache.getBalanceForToken( + networkId = networkId, + derivationPath = derivationPath, + symbol = token.symbol, + ) ?: SwapAmount(BigDecimal.ZERO, getTokenDecimals(token)) } override fun isAvailableToSwap(networkId: String): Boolean { @@ -298,12 +306,13 @@ internal class SwapInteractorImpl @Inject constructor( } private fun createEmptyAmountState( + networkId: String, fromToken: Currency, toToken: Currency, ): SwapState { val appCurrency = userWalletManager.getUserAppCurrency() - val fromTokenBalance = cache.getBalanceForToken(fromToken.symbol) - val toTokenBalance = cache.getBalanceForToken(toToken.symbol) + val fromTokenBalance = cache.getBalanceForToken(networkId, derivationPath, fromToken.symbol) + val toTokenBalance = cache.getBalanceForToken(networkId, derivationPath, toToken.symbol) return SwapState.EmptyAmountState( fromTokenWalletBalance = fromTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") }.orEmpty(), toTokenWalletBalance = toTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") }.orEmpty(), @@ -411,7 +420,7 @@ internal class SwapInteractorImpl @Inject constructor( decimals = transactionManager.getNativeTokenDecimals(networkId), currency = userWalletManager.getNetworkCurrency(networkId), ) + feeFiat - val isBalanceIncludeFeeEnough = isBalanceEnough(fromToken, amount, feeData.fee.value) + val isBalanceIncludeFeeEnough = isBalanceEnough(networkId, fromToken, amount, feeData.fee.value) val isFeeEnough = checkFeeIsEnough( fee = feeData.fee.value, spendAmount = amount, @@ -458,8 +467,8 @@ internal class SwapInteractorImpl @Inject constructor( val appCurrency = userWalletManager.getUserAppCurrency() val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId) val rates = repository.getRates(appCurrency.code, listOf(fromToken.id, toToken.id, nativeToken.id)) - val fromTokenBalance = cache.getBalanceForToken(fromToken.symbol) - val toTokenBalance = cache.getBalanceForToken(toToken.symbol) + val fromTokenBalance = cache.getBalanceForToken(networkId, derivationPath, fromToken.symbol) + val toTokenBalance = cache.getBalanceForToken(networkId, derivationPath, toToken.symbol) return SwapState.QuotesLoadedState( fromTokenInfo = TokenSwapInfo( tokenAmount = fromTokenAmount, @@ -503,7 +512,7 @@ internal class SwapInteractorImpl @Inject constructor( quotesLoadedState: SwapState.QuotesLoadedState, ): SwapState.QuotesLoadedState { // if token balance ZERO not show permission state to avoid user to spend money for fee - val isTokenZeroBalance = getTokenBalance(fromToken).value.compareTo(BigDecimal.ZERO) == 0 + val isTokenZeroBalance = getTokenBalance(networkId, fromToken).value.compareTo(BigDecimal.ZERO) == 0 if (isTokenZeroBalance) { return quotesLoadedState.copy( permissionState = PermissionDataState.Empty, @@ -546,7 +555,7 @@ internal class SwapInteractorImpl @Inject constructor( } private suspend fun syncWalletBalanceForTokens(networkId: String, tokens: List) { - val tokensToSync = tokens.filter { cache.getBalanceForToken(it.symbol) == null } + val tokensToSync = tokens.filter { cache.getBalanceForToken(networkId, derivationPath, it.symbol) == null } if (tokensToSync.isNotEmpty()) { val tokensBalance = userWalletManager.getCurrentWalletTokensBalance( @@ -554,12 +563,21 @@ internal class SwapInteractorImpl @Inject constructor( extraTokens = tokensToSync.map { cryptoCurrencyConverter.convert(it) }, derivationPath = derivationPath, ) - cache.cacheBalances(tokensBalance.mapValues { SwapAmount(it.value.value, it.value.decimals) }) + cache.cacheBalances( + networkId = networkId, + derivationPath = derivationPath, + balances = tokensBalance.mapValues { SwapAmount(it.value.value, it.value.decimals) }, + ) } } - private fun isBalanceEnough(fromToken: Currency, amount: SwapAmount, fee: BigDecimal?): Boolean { - val tokenBalance = getTokenBalance(fromToken).value + private fun isBalanceEnough( + networkId: String, + fromToken: Currency, + amount: SwapAmount, + fee: BigDecimal?, + ): Boolean { + val tokenBalance = getTokenBalance(networkId, fromToken).value return if (fromToken is Currency.NonNativeToken) { tokenBalance >= amount.value } else { diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt index 36a0ef8ef3..7f4bf919cc 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt @@ -10,11 +10,11 @@ interface SwapDataCache { fun cacheAvailableToSwapTokens(networkId: String, tokens: List) fun cacheInWalletTokens(tokens: List) fun cacheLoadedTokens(tokens: List) - fun cacheBalances(balances: Map) + fun cacheBalances(networkId: String, derivationPath: String?, balances: Map) fun cacheLastFeeForNetwork(fee: BigDecimal, networkId: String) fun getAvailableTokens(networkId: String): List fun getInWalletTokens(): List fun getLoadedTokens(): List - fun getBalanceForToken(symbol: String): SwapAmount? + fun getBalanceForToken(networkId: String, derivationPath: String?, symbol: String): SwapAmount? fun getLastFeeForNetwork(networkId: String): BigDecimal? } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt index f044693ece..4379d70d9f 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt @@ -9,7 +9,7 @@ class SwapDataCacheImpl : SwapDataCache { private val availableTokensForNetwork: MutableMap> = mutableMapOf() private val feesForNetworks: MutableMap = mutableMapOf() - private val tokensBalances: MutableMap = mutableMapOf() + private val tokensBalances: MutableMap> = mutableMapOf() private val lastInWalletTokens = mutableListOf() private val lastLoadedTokens = mutableListOf() @@ -35,12 +35,12 @@ class SwapDataCacheImpl : SwapDataCache { return lastLoadedTokens } - override fun getBalanceForToken(symbol: String): SwapAmount? { - return tokensBalances[symbol] + override fun getBalanceForToken(networkId: String, derivationPath: String?, symbol: String): SwapAmount? { + return tokensBalances[createKeyFrom(networkId, derivationPath)]?.get(symbol) } - override fun cacheBalances(balances: Map) { - tokensBalances.putAll(balances) + override fun cacheBalances(networkId: String, derivationPath: String?, balances: Map) { + tokensBalances[createKeyFrom(networkId, derivationPath)] = balances } override fun cacheAvailableToSwapTokens(networkId: String, tokens: List) { @@ -54,4 +54,8 @@ class SwapDataCacheImpl : SwapDataCache { override fun getAvailableTokens(networkId: String): List { return availableTokensForNetwork.getOrElse(networkId) { emptyList() } } + + private fun createKeyFrom(networkId: String, derivationPath: String?): String { + return "$networkId;$derivationPath" + } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index b18ceab795..e441f3bf63 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -371,7 +371,7 @@ internal class SwapViewModel @Inject constructor( private fun onMaxAmountClicked() { dataState.fromCurrency?.let { - val balance = swapInteractor.getTokenBalance(it) + val balance = swapInteractor.getTokenBalance(currency.networkId, it) onAmountChanged(balance.formatToUIRepresentation()) } } diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt index e9c0d1beea..23659c303f 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt @@ -13,7 +13,7 @@ interface UserWalletManager { * Returns all user tokens (merged from local and backend) */ @Throws(IllegalStateException::class) - suspend fun getUserTokens(networkId: String, isExcludeCustom: Boolean): List + suspend fun getUserTokens(networkId: String, derivationPath: String?, isExcludeCustom: Boolean): List @Throws(IllegalStateException::class) fun getNativeTokenForNetwork(networkId: String): Currency @@ -55,6 +55,7 @@ interface UserWalletManager { * Return balances from wallet found by networkId * * @param networkId + * @param extraTokens tokens you want to check balance that not exists in wallet * @param derivationPath if null uses default * @return map of */