diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index b68196f43b..af0ce672e0 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -2,9 +2,11 @@ package com.tangem.tap.domain.userWalletList.repository import arrow.core.Either import arrow.core.left +import arrow.core.raise.Raise import arrow.core.raise.either import arrow.core.right import com.tangem.common.* +import com.tangem.common.core.TangemSdkError import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getSyncOrDefault @@ -237,7 +239,7 @@ internal class DefaultUserWalletsListRepository( } UserWalletsListRepository.UnlockMethod.AccessCode -> { if (userWallet !is UserWallet.Hot) { - raise(UnlockWalletError.UnableToUnlock) + raise(UnlockWalletError.UnableToUnlock.Empty) } val encryptionKey = requestPasswordRecursive( @@ -245,8 +247,8 @@ internal class DefaultUserWalletsListRepository( block = { password -> runSuspendCatching { userWalletEncryptionKeysRepository.getEncryptedWithPassword(userWalletId, password) - }.onFailure { - raise(UnlockWalletError.UnableToUnlock) + }.onFailure { error -> + raise(UnlockWalletError.UnableToUnlock.RawException(error)) }.getOrNull() }, biometryFallback = { @@ -262,13 +264,11 @@ internal class DefaultUserWalletsListRepository( sensitiveInformationRepository.getAll(listOf(encryptionKey)) .doOnSuccess { sensitiveInfo -> updateWallets { it?.updateWith(sensitiveInfo) } } - .doOnFailure { error -> - raise(UnlockWalletError.UnableToUnlock) - } + .doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock.RawException(error)) } } is UserWalletsListRepository.UnlockMethod.Scan -> { if (userWallet !is UserWallet.Cold) { - raise(UnlockWalletError.UnableToUnlock) + raise(UnlockWalletError.UnableToUnlock.Empty) } val scanResponse = unlockMethod.scanResponse ?: run { @@ -287,12 +287,12 @@ internal class DefaultUserWalletsListRepository( val encryptionKey = UserWalletEncryptionKey( walletId = userWallet.walletId, - encryptionKey = scanResponse.encryptionKey ?: raise(UnlockWalletError.UnableToUnlock), + encryptionKey = scanResponse.encryptionKey ?: raise(UnlockWalletError.UnableToUnlock.Empty), ) sensitiveInformationRepository.getAll(listOf(encryptionKey)) .doOnSuccess { sensitiveInfo -> updateWallets { it?.updateWith(sensitiveInfo) } } - .doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock) } + .doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock.RawException(error)) } } } } @@ -306,9 +306,8 @@ internal class DefaultUserWalletsListRepository( val biometricKeys = runSuspendCatching { userWalletEncryptionKeysRepository.getAllBiometric() - }.getOrElse { - // TODO handle error properly [REDACTED_TASK_KEY] - raise(UnlockWalletError.UserCancelled) + }.getOrElse { exception -> + catchBiometricException(exception) } val unsecuredKeys = userWalletEncryptionKeysRepository.getAllUnsecured() @@ -327,14 +326,14 @@ internal class DefaultUserWalletsListRepository( // if we cant unlock any of the locked wallets, return error // (isLocked remains `true` here because we haven't updated the wallets yet) if (unlockedWallets.any { it.isLocked }.not()) { - raise(UnlockWalletError.UnableToUnlock) + raise(UnlockWalletError.UnableToUnlock.Empty) } sensitiveInformationRepository.getAll(allKeys) .doOnSuccess { sensitiveInfo -> updateWallets { wallets -> wallets?.updateWith(sensitiveInfo) } } - .doOnFailure { raise(UnlockWalletError.UnableToUnlock) } + .doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock.RawException(error)) } } override suspend fun lockAllWallets(): Either = either { @@ -460,6 +459,27 @@ internal class DefaultUserWalletsListRepository( } } + private fun Raise.catchBiometricException(ex: Throwable): Nothing { + val reason = when (ex) { + is TangemSdkError.AuthenticationLockout -> + UnlockWalletError.UnableToUnlock.Reason.BiometricsAuthenticationLockout(isPermanent = false) + is TangemSdkError.AuthenticationPermanentLockout -> + UnlockWalletError.UnableToUnlock.Reason.BiometricsAuthenticationLockout(isPermanent = true) + is TangemSdkError.KeystoreInvalidated -> + UnlockWalletError.UnableToUnlock.Reason.AllKeysInvalidated + is TangemSdkError.AuthenticationUnavailable -> + UnlockWalletError.UnableToUnlock.Reason.BiometricsAuthenticationDisabled + + is TangemSdkError.AuthenticationCanceled, + is TangemSdkError.AuthenticationAlreadyInProgress, + -> raise(UnlockWalletError.UserCancelled) + + else -> raise(UnlockWalletError.UnableToUnlock.RawException(ex)) + } + + raise(UnlockWalletError.UnableToUnlock.WithReason(reason)) + } + /** * Find the nearest available wallet that can be selected * diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts index 2cb8f794be..9583112c85 100644 --- a/common/ui/build.gradle.kts +++ b/common/ui/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.onramp.models) implementation(projects.domain.promo.models) + implementation(projects.domain.common) implementation(tangemDeps.card.core) implementation(tangemDeps.blockchain) { diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt new file mode 100644 index 0000000000..851972f960 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt @@ -0,0 +1,78 @@ +package com.tangem.common.ui.userwallet + +import com.tangem.common.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessage +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.common.wallets.error.UnlockWalletError +import com.tangem.domain.common.wallets.error.UnlockWalletError.UnableToUnlock.Reason + +inline fun UnlockWalletError.handle( + onAlreadyUnlocked: () -> Unit = {}, + onUserCancelled: () -> Unit = {}, + noinline showMessage: (EventMessage) -> Unit, +) { + when (this) { + UnlockWalletError.AlreadyUnlocked -> onAlreadyUnlocked() + UnlockWalletError.ScannedCardWalletNotMatched -> { + showMessage( + DialogMessage( + title = resourceReference(R.string.common_warning), + message = resourceReference(R.string.error_wrong_wallet_tapped), + ), + ) + } + UnlockWalletError.UserCancelled -> onUserCancelled() + UnlockWalletError.UserWalletNotFound -> { + // This should never happen in this flow, as we always check for the wallet existence before unlocking + showMessage(SnackbarMessage(TextReference.Res(R.string.generic_error))) + } + is UnlockWalletError.UnableToUnlock -> handleUnableToUnlock(this, showMessage) + } +} + +fun handleUnableToUnlock(error: UnlockWalletError.UnableToUnlock, showDialog: (DialogMessage) -> Unit) { + val dialogMessage = when (error) { + is UnlockWalletError.UnableToUnlock.WithReason -> { + when (error.reason) { + Reason.AllKeysInvalidated -> { + DialogMessage( + title = resourceReference(R.string.biometric_updated_warning_title), + message = resourceReference(R.string.biometric_updated_warning_description), + ) + } + Reason.BiometricsAuthenticationDisabled -> { + DialogMessage( + title = resourceReference(R.string.biometric_disabled_warning_title), + message = resourceReference(R.string.biometric_disabled_warning_description), + ) + } + is Reason.BiometricsAuthenticationLockout -> { + if ((error.reason as Reason.BiometricsAuthenticationLockout).isPermanent) { + DialogMessage( + title = resourceReference(R.string.biometric_lockout_permanent_warning_title), + message = resourceReference(R.string.biometric_lockout_permanent_warning_description_2), + ) + } else { + DialogMessage( + title = resourceReference(R.string.biometric_lockout_warning_title), + message = resourceReference(R.string.biometric_lockout_warning_description_2), + ) + } + } + } + } + is UnlockWalletError.UnableToUnlock.RawException -> { + DialogMessage( + title = resourceReference(R.string.common_something_went_wrong), + message = stringReference(error.throwable.toString()), + ) + } + UnlockWalletError.UnableToUnlock.Empty -> return + } + + showDialog(dialogMessage) +} \ No newline at end of file diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index a6478e3318..4df1c486bf 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -1420,9 +1420,14 @@ あなたの住所や資産を開示することなく、個別の支払い用アカウントが作成されます。 他に類を見ないプライバシー 無料のTangem Payカードを数分でゲットしましょう + 支払いアカウント + 支払いアカウントの同期が必要です 技術的な問題を修正しています。後でもう一度お試しください。 サービスは一時的に利用できません + 現在サービスに接続できません。後ほどもう一度お試しください。 + Tangem Payは一時的に利用できません Tangem Pay + カードまたはリングを使用して、支払いアカウントへのアクセスを復元してください。 これは私のウォレットです 残高非表示 残高表示 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 6eaea7b176..8967ea964b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -139,10 +139,17 @@ Balances are hidden According to the blockchain developers, Kaspa tokens are currently in beta. Stay tuned for updates! Beta Mode + Biometrics are turned off on your device, so you can’t use them to unlock your wallets. Enable biometrics in your device settings to use this method again. + Biometric authentication disabled Please scan the card or ring + You’ve reached the limit of biometric attempts. Please unlock your wallet with a device tap or enter your access code. + Biometric authentication locked Please try again in 30 seconds or scan the card or ring + Biometric login is temporarily locked. Please try again in 30 seconds, or unlock your wallet with a device tap or access code. Too many attempts You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings. + Biometrics on your device have been updated. Please select your wallet and enter its access code to enable biometric login again. + Attention required An error occurred while processing your promo code. Please try again later. Activation error Your promo code was successfully activated. The reward will be credited to your Bitcoin account within 14 days. @@ -835,6 +842,8 @@ Destination account is not active. Send %s or more to activate the account. To create account send funds to this address The destination account does not have a trustline for the asset being sent. + Plus Rewards in BTC per Set.\nHurry up + Black Friday: up to 25% OFF Join Now Share your code - earn 5 USDT per sale. Your friend gets 10% OFF. Get REWARDS for every friend! diff --git a/domain/common/src/main/java/com/tangem/domain/common/wallets/error/UnlockWalletError.kt b/domain/common/src/main/java/com/tangem/domain/common/wallets/error/UnlockWalletError.kt index c1f6e1fe89..a1fcb1aa6b 100644 --- a/domain/common/src/main/java/com/tangem/domain/common/wallets/error/UnlockWalletError.kt +++ b/domain/common/src/main/java/com/tangem/domain/common/wallets/error/UnlockWalletError.kt @@ -6,7 +6,20 @@ sealed interface UnlockWalletError { data object UserWalletNotFound : UnlockWalletError - data object UnableToUnlock : UnlockWalletError + sealed class UnableToUnlock : UnlockWalletError { + + data object Empty : UnableToUnlock() + + data class RawException(val throwable: Throwable) : UnableToUnlock() + + data class WithReason(val reason: Reason) : UnableToUnlock() + + sealed class Reason { + data class BiometricsAuthenticationLockout(val isPermanent: Boolean) : Reason() + data object AllKeysInvalidated : Reason() + data object BiometricsAuthenticationDisabled : Reason() + } + } data object UserCancelled : UnlockWalletError diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index 511bcb27d8..923627f080 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.details.model import com.tangem.common.routing.AppRoute +import com.tangem.common.ui.userwallet.handle import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -98,11 +99,14 @@ internal class UserWalletListModel @Inject constructor( if (hotWalletFeatureToggles.isHotWalletEnabled) { modelScope.launch { unlockWalletUseCase(userWalletId) - .onRight { - router.push(AppRoute.WalletSettings(userWalletId)) - } + .onRight { router.push(AppRoute.WalletSettings(userWalletId)) } .onLeft { error -> Timber.e("Failed to unlock wallet $userWalletId: $error") + error.handle( + onUserCancelled = {}, + onAlreadyUnlocked = { router.push(AppRoute.WalletSettings(userWalletId)) }, + showMessage = messageSender::send, + ) } } } else { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index 8c868fcabf..e078030757 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -5,20 +5,17 @@ import com.tangem.common.TangemBlogUrlBuilder import com.tangem.common.routing.AppRoute.* import com.tangem.common.routing.AppRouter import com.tangem.common.ui.notifications.NotificationId +import com.tangem.common.ui.userwallet.handle import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.components.bottomsheets.message.* -import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.message.DialogMessage -import com.tangem.core.ui.message.SnackbarMessage import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.domain.card.SetCardWasScannedUseCase import com.tangem.domain.common.wallets.UserWalletsListRepository -import com.tangem.domain.common.wallets.error.UnlockWalletError import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType @@ -202,32 +199,12 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( .onLeft { val selectedUserWalletId = stateHolder.getSelectedWalletId() nonBiometricUnlockWalletUseCase(selectedUserWalletId) - .onLeft { - when (it) { - UnlockWalletError.AlreadyUnlocked -> Unit - UnlockWalletError.ScannedCardWalletNotMatched -> { - uiMessageSender.send( - message = DialogMessage( - title = resourceReference(R.string.common_warning), - message = resourceReference(R.string.error_wrong_wallet_tapped), - ), - ) - } - UnlockWalletError.UnableToUnlock -> { - Timber.e("Unable to unlock wallet with id: $selectedUserWalletId") - uiMessageSender.send( - SnackbarMessage(TextReference.Res(R.string.generic_error)), - ) - } - UnlockWalletError.UserCancelled -> Unit - UnlockWalletError.UserWalletNotFound -> { - // This should never happen in this flow - Timber.e("User wallet not found for unlock: $selectedUserWalletId") - uiMessageSender.send( - SnackbarMessage(TextReference.Res(R.string.generic_error)), - ) - } - } + .onLeft { error -> + error.handle( + onAlreadyUnlocked = {}, + onUserCancelled = {}, + showMessage = uiMessageSender::send, + ) } } } diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index 926b325964..bf40c9be77 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -1,15 +1,12 @@ package com.tangem.features.welcome.impl.model import com.tangem.common.routing.AppRoute +import com.tangem.common.ui.userwallet.handle import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.message.DialogMessage -import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.error.UnlockWalletError import com.tangem.domain.models.wallet.UserWallet @@ -19,9 +16,7 @@ import com.tangem.domain.settings.CanUseBiometryUseCase import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.NonBiometricUnlockWalletUseCase import com.tangem.features.wallet.utils.UserWalletsFetcher -import com.tangem.features.welcome.impl.R import com.tangem.features.welcome.impl.ui.state.WelcomeUM -import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn @@ -29,7 +24,6 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -150,10 +144,10 @@ internal class WelcomeModel @Inject constructor( } private suspend fun onlyOneHotWalletWithAccessCode(): Boolean { - val userWalletsWithLock = userWalletsListRepository.userWalletsSync() - if (userWalletsWithLock.size != 1) return false - val wallet = userWalletsWithLock.first() - return wallet is UserWallet.Hot && wallet.hotWalletId.authType != HotWalletId.AuthType.NoPassword + val userWallets = userWalletsListRepository.userWalletsSync() + if (userWallets.size != 1) return false + val wallet = userWallets.first() + return wallet is UserWallet.Hot && wallet.isLocked } private fun onUserWalletClick(userWallet: UserWallet) = modelScope.launch { @@ -189,32 +183,15 @@ internal class WelcomeModel @Inject constructor( } suspend fun UnlockWalletError.handle(specificWalletId: UserWalletId?, onUserCancelled: suspend () -> Unit = { }) { - when (this) { - UnlockWalletError.AlreadyUnlocked -> { + handle( + onAlreadyUnlocked = { // this should not happen, as we check for locked state before this specificWalletId?.let { userWalletsListRepository.select(it) } router.replaceAll(AppRoute.Wallet) - } - UnlockWalletError.ScannedCardWalletNotMatched -> { - uiMessageSender.send( - message = DialogMessage( - title = resourceReference(R.string.common_warning), - message = resourceReference(R.string.error_wrong_wallet_tapped), - ), - ) - } - UnlockWalletError.UnableToUnlock -> { - // TODO Unable to unlock the wallet" - } - UnlockWalletError.UserCancelled -> onUserCancelled() - UnlockWalletError.UserWalletNotFound -> { - // This should never happen in this flow, as we always check for the wallet existence before unlocking - Timber.e("User wallet not found for unlock: $specificWalletId") - uiMessageSender.send( - SnackbarMessage(TextReference.Res(R.string.generic_error)), - ) - } - } + }, + onUserCancelled = { onUserCancelled() }, + showMessage = uiMessageSender::send, + ) } private fun updateSelectState(block: (WelcomeUM.SelectWallet) -> WelcomeUM.SelectWallet) {