From 0be13587603605bb718cb43af46965c1c74ed707 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Jun 2026 12:36:46 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../tangem/tap/di/domain/CardDomainModule.kt | 12 +++ .../tap/di/domain/FeedbackDomainModule.kt | 13 +++ common/ui-markets/build.gradle.kts | 2 + .../ui/markets/action/TokenActionsHandler.kt | 24 +++++- .../common/ui/notifications/NotificationUM.kt | 9 ++ .../components/notifications/Notification.kt | 6 ++ .../tangem/core/ui/message/dialog/Dialogs.kt | 18 ++++ .../tangem/core/ui/res/TangemColorPalette.kt | 1 + domain/account/status/build.gradle.kts | 1 + .../status/di/AccountStatusUseCaseModule.kt | 16 ++++ ...ackupProblematicWalletForAddressUseCase.kt | 24 ++++++ ...pProblematicWalletForAddressUseCaseTest.kt | 82 +++++++++++++++++++ .../com/tangem/domain/card/BackupValidator.kt | 2 +- .../card/IsWalletBackupProblematicUseCase.kt | 23 ++++++ .../IsWalletBackupProblematicUseCaseTest.kt | 61 ++++++++++++++ domain/feedback/build.gradle.kts | 3 + .../feedback/SendBackupProblemEmailUseCase.kt | 15 ++++ .../SendBackupProblemEmailUseCaseTest.kt | 57 +++++++++++++ .../transaction/error/ValidateAddressError.kt | 1 + .../child/finalize/model/BackupValidator.kt | 47 ----------- .../model/MultiWalletFinalizeModel.kt | 2 +- .../model/MultiWalletFinalizeModelTest.kt | 8 +- .../SendDestinationAlertFactory.kt | 13 +++ .../destination/model/SendDestinationModel.kt | 37 ++++++++- ...dDestinationValidationResultTransformer.kt | 19 +++-- .../model/SwapNotificationsModel.kt | 28 +++++++ features/swap/impl/build.gradle.kts | 1 + .../tangem/feature/swap/model/SwapModel.kt | 19 +++++ .../feature/swap/model/SwapModelTestBase.kt | 6 ++ features/tokendetails/impl/build.gradle.kts | 1 + .../model/TokenDetailsDialogFactory.kt | 5 ++ .../tokendetails/model/TokenDetailsModel.kt | 20 +++++ .../WalletCurrencyActionsClickIntents.kt | 34 ++++++++ .../wallet/domain/BackupValidator.kt | 47 ----------- .../domain/GetMultiWalletWarningsFactory.kt | 21 +++-- .../domain/GetWalletNotificationsFactory.kt | 5 +- .../wallet/state/model/WalletAlertUM.kt | 5 ++ .../wallet/state/model/WalletNotification.kt | 6 +- .../components/common/WalletNotifications.kt | 11 +++ 39 files changed, 581 insertions(+), 124 deletions(-) create mode 100644 domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetBackupProblematicWalletForAddressUseCase.kt create mode 100644 domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetBackupProblematicWalletForAddressUseCaseTest.kt create mode 100644 domain/card/src/main/kotlin/com/tangem/domain/card/IsWalletBackupProblematicUseCase.kt create mode 100644 domain/card/src/test/java/com/tangem/domain/card/IsWalletBackupProblematicUseCaseTest.kt create mode 100644 domain/feedback/src/main/java/com/tangem/domain/feedback/SendBackupProblemEmailUseCase.kt create mode 100644 domain/feedback/src/test/java/com/tangem/domain/feedback/SendBackupProblemEmailUseCaseTest.kt delete mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/BackupValidator.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index 45b884766c..cef5171d26 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -1,6 +1,8 @@ package com.tangem.tap.di.domain +import com.tangem.domain.card.BackupValidator import com.tangem.domain.card.DeleteSavedAccessCodesUseCase +import com.tangem.domain.card.IsWalletBackupProblematicUseCase import com.tangem.domain.card.ResetCardUseCase import com.tangem.domain.card.SetCardWasScannedUseCase import com.tangem.domain.card.repository.CardRepository @@ -32,6 +34,16 @@ internal object CardDomainModule { @Singleton fun provideIsDemoCardUseCase(): IsDemoCardUseCase = IsDemoCardUseCase(config = DemoConfig) + @Provides + @Singleton + fun provideBackupValidator(): BackupValidator = BackupValidator() + + @Provides + @Singleton + fun provideIsWalletBackupProblematicUseCase(backupValidator: BackupValidator): IsWalletBackupProblematicUseCase { + return IsWalletBackupProblematicUseCase(backupValidator = backupValidator) + } + @Provides @Singleton fun provideDerivePublicKeysUseCase(derivationsRepository: DerivationsRepository): DerivePublicKeysUseCase { diff --git a/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt index 48bfb0b921..040f65f923 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt @@ -3,6 +3,7 @@ package com.tangem.tap.di.domain import android.content.Context import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase +import com.tangem.domain.feedback.SendBackupProblemEmailUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.repository.FeedbackRepository import dagger.Module @@ -36,4 +37,16 @@ internal object FeedbackDomainModule { fun provideSaveBlockchainErrorUseCase(feedbackRepository: FeedbackRepository): SaveBlockchainErrorUseCase { return SaveBlockchainErrorUseCase(feedbackRepository = feedbackRepository) } + + @Provides + @Singleton + fun provideSendBackupProblemEmailUseCase( + getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, + sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + ): SendBackupProblemEmailUseCase { + return SendBackupProblemEmailUseCase( + getWalletMetaInfoUseCase = getWalletMetaInfoUseCase, + sendFeedbackEmailUseCase = sendFeedbackEmailUseCase, + ) + } } \ No newline at end of file diff --git a/common/ui-markets/build.gradle.kts b/common/ui-markets/build.gradle.kts index 7eb6c53bd1..c665f48b03 100644 --- a/common/ui-markets/build.gradle.kts +++ b/common/ui-markets/build.gradle.kts @@ -31,6 +31,8 @@ dependencies { implementation(projects.domain.onramp.models) implementation(projects.domain.offramp) implementation(projects.domain.demo) + implementation(projects.domain.card) + implementation(projects.domain.feedback) implementation(deps.lifecycle.compose) implementation(deps.compose.foundation) diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsHandler.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsHandler.kt index 962a2334fe..1680b7253d 100644 --- a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsHandler.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsHandler.kt @@ -12,9 +12,12 @@ import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.dialog.Dialogs import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.card.IsWalletBackupProblematicUseCase import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.feedback.SendBackupProblemEmailUseCase import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.offramp.GetOfframpUrlUseCase import com.tangem.domain.onramp.model.OnrampSource @@ -23,9 +26,9 @@ import com.tangem.utils.Provider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import kotlinx.collections.immutable.toImmutableList @Suppress("LongParameterList") class TokenActionsHandler @AssistedInject constructor( @@ -39,6 +42,8 @@ class TokenActionsHandler @AssistedInject constructor( @Assisted private val onHandleQuickAction: (action: HandledQuickAction, shouldDismiss: Boolean) -> Unit, @Assisted private val coroutineScope: CoroutineScope, private val isDemoCardUseCase: IsDemoCardUseCase, + private val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase, + private val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase, private val messageSender: UiMessageSender, ) { @@ -47,6 +52,8 @@ class TokenActionsHandler @AssistedInject constructor( } fun handle(action: TokenActionsBSContentUM.Action, cryptoCurrencyData: CryptoCurrencyData) { + if (isTopUpBlockedByBackupError(action, cryptoCurrencyData.userWallet)) return + onHandleQuickAction( HandledQuickAction( action = action, @@ -80,6 +87,21 @@ class TokenActionsHandler @AssistedInject constructor( } } + private fun isTopUpBlockedByBackupError(action: TokenActionsBSContentUM.Action, userWallet: UserWallet): Boolean { + val isBlockedAction = action == TokenActionsBSContentUM.Action.Buy || + action == TokenActionsBSContentUM.Action.Receive || + action == TokenActionsBSContentUM.Action.Exchange + if (!isBlockedAction) return false + if (!isWalletBackupProblematicUseCase(userWallet)) return false + + messageSender.send( + Dialogs.backupErrorAddFundsDisabled( + onContactSupport = { coroutineScope.launch { sendBackupProblemEmailUseCase(userWallet.walletId) } }, + ), + ) + return true + } + private fun handleDemoMode(action: TokenActionsBSContentUM.Action, userWallet: UserWallet.Cold): Boolean { val isDemoCard = isDemoCardUseCase.invoke(userWallet.cardId) val isNeededShowDemoWarning = isDemoCard && disabledActionsInDemoMode.contains(action) diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt index 893ca50431..b7fc3cc35a 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -41,6 +41,15 @@ sealed class NotificationUM(val config: NotificationConfig) { subtitle = resourceReference(R.string.send_notification_invalid_amount_text), ) + data class DestinationBackupError(val onContactSupport: () -> Unit) : Error( + title = resourceReference(R.string.warning_backup_error_add_funds_title), + subtitle = resourceReference(R.string.warning_backup_error_add_funds_message), + buttonState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.common_contact_support), + onClick = onContactSupport, + ), + ) + data class MinimumAmountError(val amount: String) : Error( title = resourceReference(R.string.send_notification_invalid_amount_title), subtitle = resourceReference( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index aecdc07427..5552f95042 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -4,6 +4,7 @@ import android.content.res.Configuration import androidx.annotation.DrawableRes import coil.compose.SubcomposeAsyncImage import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.clickable @@ -28,6 +29,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.* import com.tangem.core.ui.components.buttons.common.TangemButtonSize @@ -61,6 +63,7 @@ fun Notification( titleColor: Color = TangemTheme.colors.text.primary1, subtitleColor: Color = TangemTheme.colors.text.tertiary, containerColor: Color? = null, + borderColor: Color? = null, iconTint: Color? = when (config.iconTint) { NotificationConfig.IconTint.Unspecified -> null NotificationConfig.IconTint.Accent -> TangemTheme.colors.icon.accent @@ -75,6 +78,7 @@ fun Notification( onCloseClick = config.onCloseClick, modifier = modifier, containerColor = containerColor, + borderColor = borderColor, isEnabled = isEnabled, ) { MainContent( @@ -100,6 +104,7 @@ internal fun NotificationBaseContainer( modifier: Modifier = Modifier, isEnabled: Boolean = true, containerColor: Color? = null, + borderColor: Color? = null, content: @Composable ColumnScope.() -> Unit, ) { val tempContainerColor by rememberUpdatedState( @@ -118,6 +123,7 @@ internal fun NotificationBaseContainer( enabled = onClick != null && isEnabled, shape = TangemTheme.shapes.roundedCornersXMedium, color = containerColor ?: tempContainerColor, + border = borderColor?.let { BorderStroke(width = 1.dp, color = it) }, ) { Box { Column( diff --git a/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt index ff431ff036..c96f831a79 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt @@ -79,6 +79,24 @@ object Dialogs { ) } + /** + * Dialog shown when adding funds (buy / receive / swap) is blocked because the wallet has a backup problem. + * Centralizes the wording and button behavior reused across wallet actions, token details, markets and swap. + * + * @param onContactSupport lambda invoked when the "Contact support" action is clicked + */ + fun backupErrorAddFundsDisabled(onContactSupport: () -> Unit): DialogMessage = DialogMessage( + title = resourceReference(R.string.warning_backup_error_add_funds_title), + message = resourceReference(R.string.warning_backup_error_add_funds_message), + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_contact_support), + onClick = onContactSupport, + ) + }, + secondActionBuilder = { cancelAction() }, + ) + /** * Universal error dialog */ diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt index 5b4ef02f7d..4ce661f796 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt @@ -72,6 +72,7 @@ object TangemColorPalette { // region Amaranth val Amaranth = Color(0xFFFF3333) val Amaranth_50 = Color(0x80FF3333) + val Amaranth_30 = Color(0x4DFF3333) val Amaranth_20 = Color(0x33FF3333) val Amaranth_10 = Color(0x1AFF3333) // endregion Amaranth diff --git a/domain/account/status/build.gradle.kts b/domain/account/status/build.gradle.kts index 34703992ad..d7415a2a8a 100644 --- a/domain/account/status/build.gradle.kts +++ b/domain/account/status/build.gradle.kts @@ -12,6 +12,7 @@ android { } dependencies { api(projects.domain.account) + api(projects.domain.card) api(projects.domain.core) api(projects.domain.common) api(projects.domain.express) diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt index 2966a613f4..859541ef01 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt @@ -8,6 +8,7 @@ import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher import com.tangem.domain.account.status.utils.CryptoCurrencyMetadataCleaner import com.tangem.domain.account.supplier.MultiAccountListSupplier import com.tangem.domain.account.supplier.SingleAccountListSupplier +import com.tangem.domain.card.IsWalletBackupProblematicUseCase import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.express.ExpressServiceFetcher @@ -24,6 +25,7 @@ import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.derivations.DerivationsRepository +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -50,6 +52,20 @@ internal object AccountStatusUseCaseModule { ) } + @Provides + @Singleton + fun provideGetBackupProblematicWalletForAddressUseCase( + getAccountCurrencyByAddressUseCase: GetAccountCurrencyByAddressUseCase, + getUserWalletUseCase: GetUserWalletUseCase, + isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase, + ): GetBackupProblematicWalletForAddressUseCase { + return GetBackupProblematicWalletForAddressUseCase( + getAccountCurrencyByAddressUseCase = getAccountCurrencyByAddressUseCase, + getUserWalletUseCase = getUserWalletUseCase, + isWalletBackupProblematicUseCase = isWalletBackupProblematicUseCase, + ) + } + @Provides @Singleton fun provideIsAccountsModeEnabledUseCase( diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetBackupProblematicWalletForAddressUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetBackupProblematicWalletForAddressUseCase.kt new file mode 100644 index 0000000000..5e647e41db --- /dev/null +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetBackupProblematicWalletForAddressUseCase.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.account.status.usecase + +import com.tangem.domain.card.IsWalletBackupProblematicUseCase +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase + +/** + * Resolves [address] to one of the user's own wallets and returns its [UserWalletId] when that wallet + * has a backup error (e.g. a CardLinked card), or `null` otherwise. + * + * Shared by the send and swap flows to block topping up a wallet with a backup problem. + */ +class GetBackupProblematicWalletForAddressUseCase( + private val getAccountCurrencyByAddressUseCase: GetAccountCurrencyByAddressUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase, +) { + + suspend operator fun invoke(address: String): UserWalletId? { + val walletId = getAccountCurrencyByAddressUseCase(address).getOrNull()?.account?.userWalletId ?: return null + val wallet = getUserWalletUseCase(walletId).getOrNull() ?: return null + return walletId.takeIf { isWalletBackupProblematicUseCase(wallet) } + } +} \ No newline at end of file diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetBackupProblematicWalletForAddressUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetBackupProblematicWalletForAddressUseCaseTest.kt new file mode 100644 index 0000000000..f20cd59184 --- /dev/null +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetBackupProblematicWalletForAddressUseCaseTest.kt @@ -0,0 +1,82 @@ +package com.tangem.domain.account.status.usecase + +import arrow.core.left +import arrow.core.none +import arrow.core.right +import arrow.core.some +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.account.status.model.AccountCryptoCurrency +import com.tangem.domain.card.IsWalletBackupProblematicUseCase +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.models.GetUserWalletError +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class GetBackupProblematicWalletForAddressUseCaseTest { + + private val getAccountCurrencyByAddressUseCase: GetAccountCurrencyByAddressUseCase = mockk() + private val getUserWalletUseCase: GetUserWalletUseCase = mockk() + private val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase = mockk() + + private val useCase = GetBackupProblematicWalletForAddressUseCase( + getAccountCurrencyByAddressUseCase = getAccountCurrencyByAddressUseCase, + getUserWalletUseCase = getUserWalletUseCase, + isWalletBackupProblematicUseCase = isWalletBackupProblematicUseCase, + ) + + @AfterEach + fun tearDown() { + clearMocks(getAccountCurrencyByAddressUseCase, getUserWalletUseCase, isWalletBackupProblematicUseCase) + } + + @Test + fun `returns null when address is not resolved to an account`() = runTest { + coEvery { getAccountCurrencyByAddressUseCase(ADDRESS) } returns none() + + assertThat(useCase(ADDRESS)).isNull() + } + + @Test + fun `returns null when destination wallet is not found`() = runTest { + coEvery { getAccountCurrencyByAddressUseCase(ADDRESS) } returns accountCurrency.some() + every { getUserWalletUseCase(walletId) } returns GetUserWalletError.UserWalletNotFound.left() + + assertThat(useCase(ADDRESS)).isNull() + } + + @Test + fun `returns null when destination wallet is not problematic`() = runTest { + coEvery { getAccountCurrencyByAddressUseCase(ADDRESS) } returns accountCurrency.some() + every { getUserWalletUseCase(walletId) } returns userWallet.right() + every { isWalletBackupProblematicUseCase(userWallet) } returns false + + assertThat(useCase(ADDRESS)).isNull() + } + + @Test + fun `returns wallet id when destination wallet is problematic`() = runTest { + coEvery { getAccountCurrencyByAddressUseCase(ADDRESS) } returns accountCurrency.some() + every { getUserWalletUseCase(walletId) } returns userWallet.right() + every { isWalletBackupProblematicUseCase(userWallet) } returns true + + assertThat(useCase(ADDRESS)).isEqualTo(walletId) + } + + private companion object { + const val ADDRESS = "0x1234567890abcdef" + val walletId = UserWalletId("011") + val userWallet = mockk() + val accountCurrency = mockk { + every { account } returns mockk { every { userWalletId } returns walletId } + } + } +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/BackupValidator.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/BackupValidator.kt index 116b0060b5..0e8898465a 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/BackupValidator.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/BackupValidator.kt @@ -4,7 +4,7 @@ import com.tangem.common.card.EllipticCurve import com.tangem.domain.card.configs.CardConfig import com.tangem.domain.models.scan.CardDTO -object BackupValidator { +class BackupValidator { fun isValidFull(cardDTO: CardDTO): Boolean { return validateBackupStatus(cardDTO) && validateCurves(cardDTO) diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/IsWalletBackupProblematicUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/IsWalletBackupProblematicUseCase.kt new file mode 100644 index 0000000000..3f2e3c94d4 --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/IsWalletBackupProblematicUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.card + +import com.tangem.domain.models.wallet.UserWallet + +/** + * Single source of truth for "this wallet has a backup problem". + * + * A wallet is considered problematic when it is backed by a physical card whose backup was not + * completed correctly (e.g. a card stuck in the `CardLinked` state) or when a backup error has + * been persisted for it. Such wallets must show a prominent warning and have all top-up + * operations blocked until the user resolves the issue with support. + * + * Local detection only. Can later be backed by a backend flag without changing call sites. + */ +class IsWalletBackupProblematicUseCase( + private val backupValidator: BackupValidator, +) { + + operator fun invoke(userWallet: UserWallet): Boolean { + if (userWallet !is UserWallet.Cold) return false + return userWallet.hasBackupError || !backupValidator.isValidBackupStatus(userWallet.scanResponse.card) + } +} \ No newline at end of file diff --git a/domain/card/src/test/java/com/tangem/domain/card/IsWalletBackupProblematicUseCaseTest.kt b/domain/card/src/test/java/com/tangem/domain/card/IsWalletBackupProblematicUseCaseTest.kt new file mode 100644 index 0000000000..8ce414412a --- /dev/null +++ b/domain/card/src/test/java/com/tangem/domain/card/IsWalletBackupProblematicUseCaseTest.kt @@ -0,0 +1,61 @@ +package com.tangem.domain.card + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.models.wallet.UserWallet +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class IsWalletBackupProblematicUseCaseTest { + + private val backupValidator: BackupValidator = mockk() + + private val useCase = IsWalletBackupProblematicUseCase(backupValidator = backupValidator) + + @BeforeEach + fun setup() { + clearMocks(backupValidator) + } + + @Test + fun `returns false for hot wallet`() { + val wallet = mockk() + + assertThat(useCase(wallet)).isFalse() + } + + @Test + fun `returns true for cold wallet with persisted backup error`() { + val wallet = coldWallet(hasBackupError = true) + every { backupValidator.isValidBackupStatus(any()) } returns true + + assertThat(useCase(wallet)).isTrue() + } + + @Test + fun `returns true for cold wallet with invalid backup status`() { + val wallet = coldWallet(hasBackupError = false) + every { backupValidator.isValidBackupStatus(any()) } returns false + + assertThat(useCase(wallet)).isTrue() + } + + @Test + fun `returns false for cold wallet without backup error and valid backup status`() { + val wallet = coldWallet(hasBackupError = false) + every { backupValidator.isValidBackupStatus(any()) } returns true + + assertThat(useCase(wallet)).isFalse() + } + + private fun coldWallet(hasBackupError: Boolean): UserWallet.Cold = mockk { + every { this@mockk.hasBackupError } returns hasBackupError + every { scanResponse } returns mockk { + every { card } returns mockk() + } + } +} \ No newline at end of file diff --git a/domain/feedback/build.gradle.kts b/domain/feedback/build.gradle.kts index 3f4978f5a9..4cf70f58e3 100644 --- a/domain/feedback/build.gradle.kts +++ b/domain/feedback/build.gradle.kts @@ -17,4 +17,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.visa.models) implementation(projects.domain.feedback.models) + + /** Testing libraries */ + testImplementation(projects.test.core) } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/SendBackupProblemEmailUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/SendBackupProblemEmailUseCase.kt new file mode 100644 index 0000000000..fde9df55d8 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/SendBackupProblemEmailUseCase.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.feedback + +import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.models.wallet.UserWalletId + +class SendBackupProblemEmailUseCase( + private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, + private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, +) { + + suspend operator fun invoke(userWalletId: UserWalletId) { + val metaInfo = getWalletMetaInfoUseCase(userWalletId).getOrNull() ?: return + sendFeedbackEmailUseCase(type = FeedbackEmailType.BackupProblem(walletMetaInfo = metaInfo)) + } +} \ No newline at end of file diff --git a/domain/feedback/src/test/java/com/tangem/domain/feedback/SendBackupProblemEmailUseCaseTest.kt b/domain/feedback/src/test/java/com/tangem/domain/feedback/SendBackupProblemEmailUseCaseTest.kt new file mode 100644 index 0000000000..825756b708 --- /dev/null +++ b/domain/feedback/src/test/java/com/tangem/domain/feedback/SendBackupProblemEmailUseCaseTest.kt @@ -0,0 +1,57 @@ +package com.tangem.domain.feedback + +import arrow.core.left +import arrow.core.right +import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.feedback.models.WalletMetaInfo +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class SendBackupProblemEmailUseCaseTest { + + private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase = mockk() + private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase = mockk(relaxed = true) + + private val useCase = SendBackupProblemEmailUseCase( + getWalletMetaInfoUseCase = getWalletMetaInfoUseCase, + sendFeedbackEmailUseCase = sendFeedbackEmailUseCase, + ) + + @AfterEach + fun tearDown() { + clearMocks(getWalletMetaInfoUseCase, sendFeedbackEmailUseCase) + } + + @Test + fun `does not send email when wallet meta info is unavailable`() = runTest { + coEvery { getWalletMetaInfoUseCase(walletId) } returns Throwable().left() + + useCase(walletId) + + coVerify(exactly = 0) { sendFeedbackEmailUseCase(any()) } + } + + @Test + fun `sends backup problem email when wallet meta info is available`() = runTest { + val metaInfo = mockk() + coEvery { getWalletMetaInfoUseCase(walletId) } returns metaInfo.right() + + useCase(walletId) + + coVerify(exactly = 1) { + sendFeedbackEmailUseCase(FeedbackEmailType.BackupProblem(walletMetaInfo = metaInfo)) + } + } + + private companion object { + val walletId = UserWalletId("011") + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateAddressError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateAddressError.kt index f8aff8be0d..240e22cfd4 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateAddressError.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateAddressError.kt @@ -14,6 +14,7 @@ sealed class AddressValidation { sealed class Error : AddressValidation() { data object AddressInWallet : Error() data object InvalidAddress : Error() + data object RecipientWalletBackupError : Error() data class DataError(val throwable: Throwable) : Error() } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/BackupValidator.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/BackupValidator.kt deleted file mode 100644 index 8069228b78..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/BackupValidator.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.model - -import com.tangem.common.card.EllipticCurve -import com.tangem.domain.card.configs.CardConfig -import com.tangem.domain.models.scan.CardDTO -import javax.inject.Inject - -class BackupValidator @Inject constructor() { - - fun isValidFull(cardDTO: CardDTO): Boolean { - return validateBackupStatus(cardDTO) && validateCurves(cardDTO) - } - - fun isValidBackupStatus(cardDTO: CardDTO): Boolean { - return validateBackupStatus(cardDTO) - } - - private fun validateCurves(cardDTO: CardDTO): Boolean { - val config = CardConfig.createConfig(cardDTO) - // / Since the curve `bls12381_G2_AUG` was added later into first generation of wallets, - // we cannot determine whether this curve is missing due to an error or because the user - // did not want to recreate the wallet. - val expectedCurves = config.mandatoryCurves - .filterNot { it == EllipticCurve.Bls12381G2Aug } - val curves = cardDTO.wallets.map { it.curve } - for (expectedCurve in expectedCurves) { - val cardCurvesCount = curves.count { it == expectedCurve } - - // missing curve - if (cardCurvesCount == 0) { - return false - } - - // duplicated curve - if (cardCurvesCount > 1) { - return false - } - } - return true - } - - private fun validateBackupStatus(cardDTO: CardDTO): Boolean { - val backupStatus = cardDTO.backupStatus - backupStatus ?: return true // for card with null backup status, validation should always returns true - return backupStatus !is CardDTO.BackupStatus.CardLinked - } -} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt index 8876926bd2..0726a50908 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt @@ -12,6 +12,7 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.domain.card.BackupValidator import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase @@ -212,7 +213,6 @@ internal class MultiWalletFinalizeModel @Inject constructor( backupService.proceedBackup(iconScanRes = iconScanRes) { result -> when (result) { is CompletionResult.Success -> { - val backupValidator = BackupValidator() if (backupValidator.isValidBackupStatus(CardDTO(result.data)).not()) { hasWalletBackupError = true } diff --git a/features/onboarding-v2/impl/src/test/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModelTest.kt b/features/onboarding-v2/impl/src/test/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModelTest.kt index 92840e92da..b62be6b7c8 100644 --- a/features/onboarding-v2/impl/src/test/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModelTest.kt +++ b/features/onboarding-v2/impl/src/test/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModelTest.kt @@ -6,6 +6,7 @@ import com.tangem.common.core.TangemSdkError import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.domain.card.BackupValidator import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase @@ -97,6 +98,7 @@ internal class MultiWalletFinalizeModelTest { every { backupService.backupCardsBatchIds } returns listOf(NON_RING_BATCH_ID, NON_RING_BATCH_ID) every { backupService.currentState } returns BackupService.State.FinalizingPrimaryCard coEvery { onboardingRepository.saveUnfinishedFinalizeOnboarding(any()) } just Runs + every { backupValidator.isValidBackupStatus(any()) } returns true } @Test @@ -427,9 +429,7 @@ internal class MultiWalletFinalizeModelTest { startFromFinalize = OnboardingMultiWalletState.FinalizeStage.ScanBackupFirstCard, ) every { backupService.currentState } returns BackupService.State.FinalizingBackupCard(index = 1) - - mockkConstructor(BackupValidator::class) - every { anyConstructed().isValidBackupStatus(any()) } returns true + every { backupValidator.isValidBackupStatus(any()) } returns true val card: Card = mockk(relaxed = true) val callbackSlot = slot<(CompletionResult) -> Unit>() @@ -453,8 +453,6 @@ internal class MultiWalletFinalizeModelTest { Assertions.assertEquals(MultiWalletFinalizeUM.Step.BackupDevice2, state.step) Assertions.assertEquals("backup-2-cccc".lastMaskedExpected(), state.cardNumber) Assertions.assertTrue(events.contains(MultiWalletFinalizeComponent.Event.TwoBackupCardsAdded)) - - unmockkConstructor(BackupValidator::class) } private fun String.lastMaskedExpected(): String { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/SendDestinationAlertFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/SendDestinationAlertFactory.kt index bc14f3802d..a1d7e10c78 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/SendDestinationAlertFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/SendDestinationAlertFactory.kt @@ -25,4 +25,17 @@ internal class SendDestinationAlertFactory @Inject constructor( ), ) } + + fun showRecipientBackupErrorAlert(onContactSupport: () -> Unit) { + messageSender.send( + DialogMessage( + title = resourceReference(id = R.string.warning_backup_error_add_funds_title), + message = resourceReference(id = R.string.warning_backup_error_add_funds_message), + firstAction = EventMessageAction( + title = resourceReference(R.string.common_contact_support), + onClick = onContactSupport, + ), + ), + ) + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModel.kt index 419e8a6b2b..2a14399fd0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.send.subcomponents.destination.model import androidx.compose.runtime.Stable import arrow.core.getOrElse +import arrow.core.left import com.tangem.common.routing.AppRoute import com.tangem.common.ui.navigationButtons.NavigationButton import com.tangem.common.ui.navigationButtons.NavigationUM @@ -12,12 +13,15 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier +import com.tangem.domain.account.status.usecase.GetBackupProblematicWalletForAddressUseCase import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase +import com.tangem.domain.feedback.SendBackupProblemEmailUseCase import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.CryptoCurrencyAddress import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase @@ -48,6 +52,7 @@ import com.tangem.features.send.subcomponents.destination.model.transformers.Sen import com.tangem.features.send.subcomponents.destination.model.transformers.SendDestinationValidationStartedTransformer import com.tangem.features.send.subcomponents.destination.ui.state.DestinationWalletUM import com.tangem.features.send.impl.R +import com.tangem.features.send.subcomponents.destination.SendDestinationAlertFactory import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn @@ -58,6 +63,7 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject @Stable @@ -79,6 +85,9 @@ internal class SendDestinationModel @Inject constructor( private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier, + private val getBackupProblematicWalletForAddressUseCase: GetBackupProblematicWalletForAddressUseCase, + private val sendDestinationAlertFactory: SendDestinationAlertFactory, + private val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase, ) : Model(), SendDestinationClickIntents { private val params: SendDestinationComponentParams = paramsContainer.require() @@ -95,6 +104,8 @@ internal class SendDestinationModel @Inject constructor( private val validationJobHolder = JobHolder() + private val backupProblematicWalletCache = AtomicReference?>(null) + init { configDestinationNavigation() subscribeOnQRScannerResult() @@ -288,17 +299,41 @@ internal class SendDestinationModel @Inject constructor( } } + private suspend fun resolveBackupProblematicWallet(address: String): UserWalletId? { + backupProblematicWalletCache.get()?.let { if (it.first == address) return it.second } + + return getBackupProblematicWalletForAddressUseCase(address) + .also { backupProblematicWalletCache.set(address to it) } + } + + private fun contactBackupSupport(userWalletId: UserWalletId) { + modelScope.launch { sendBackupProblemEmailUseCase(userWalletId) } + } + private fun validate(address: String, memo: String?, type: EnterAddressSource? = null) { modelScope.launch { _uiState.update(SendDestinationValidationStartedTransformer) - val addressValidationResult = validateWalletAddressUseCase( + var addressValidationResult = validateWalletAddressUseCase( userWalletId = userWalletId, network = cryptoCurrency.network, address = address, senderAddresses = senderAddresses.value, allowSelfSend = params.isAllowSelfSend, ) + + if (addressValidationResult.isRight()) { + val problematicWalletId = resolveBackupProblematicWallet(address) + if (problematicWalletId != null) { + addressValidationResult = AddressValidation.Error.RecipientWalletBackupError.left() + if (type != null) { + sendDestinationAlertFactory.showRecipientBackupErrorAlert( + onContactSupport = { contactBackupSupport(problematicWalletId) }, + ) + } + } + } + val memoValidationResult = validateWalletMemoUseCase( userWalletId = userWalletId, cryptoCurrency = cryptoCurrency, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt b/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt index dccad080f8..69adee88af 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt @@ -24,14 +24,7 @@ internal class SendDestinationValidationResultTransformer( val isValidAddress = addressValidationResult.isRight() val isValidMemo = shouldDisableMemo || memoValidationResult.isRight() - val addressErrorText = addressValidationResult.mapLeft { error -> - when (error) { - is AddressValidation.Error.DataError, - AddressValidation.Error.InvalidAddress, - -> R.string.send_recipient_address_error - AddressValidation.Error.AddressInWallet -> R.string.send_error_address_same_as_wallet - } - }.leftOrNull() + val addressErrorText = resolveAddressErrorText() val blockchainAddress = (addressValidationResult.getOrNull() as? AddressValidation.Success.ValidNamedAddress)?.blockchainAddress @@ -61,6 +54,16 @@ internal class SendDestinationValidationResultTransformer( ) } + private fun resolveAddressErrorText(): Int? = addressValidationResult.mapLeft { error -> + when (error) { + is AddressValidation.Error.DataError, + AddressValidation.Error.InvalidAddress, + -> R.string.send_recipient_address_error + AddressValidation.Error.AddressInWallet -> R.string.send_error_address_same_as_wallet + AddressValidation.Error.RecipientWalletBackupError -> R.string.warning_backup_error_add_funds_message + } + }.leftOrNull() + private fun buildMemoField( memoField: DestinationTextFieldUM.RecipientMemo?, isValidMemo: Boolean, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt index c8e796d663..0df9f650c8 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt @@ -7,7 +7,10 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.account.status.usecase.GetBackupProblematicWalletForAddressUseCase import com.tangem.domain.express.models.ExpressError +import com.tangem.domain.feedback.SendBackupProblemEmailUseCase +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.transaction.usecase.IsMemoRequiredUseCase import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger import com.tangem.features.swap.v2.impl.amount.entity.PriceImpact @@ -28,6 +31,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import java.math.BigDecimal +import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject @Suppress("LongParameterList") @@ -38,6 +42,8 @@ internal class SwapNotificationsModel @Inject constructor( private val swapNotificationsUpdateTrigger: DefaultSwapNotificationsUpdateTrigger, private val swapAmountUpdateTrigger: SwapAmountUpdateTrigger, private val isMemoRequiredUseCase: IsMemoRequiredUseCase, + private val getBackupProblematicWalletForAddressUseCase: GetBackupProblematicWalletForAddressUseCase, + private val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase, private val analyticsEventHandler: AnalyticsEventHandler, paramsContainer: ParamsContainer, ) : Model() { @@ -47,6 +53,8 @@ internal class SwapNotificationsModel @Inject constructor( private var notificationData = params.swapNotificationData private var lastSentErrorKeys: Set>> = emptySet() + private val backupProblematicWalletCache = AtomicReference?>(null) + val uiState: StateFlow> field = MutableStateFlow>(persistentListOf()) @@ -73,6 +81,7 @@ internal class SwapNotificationsModel @Inject constructor( addInsufficientFundsNotification() addExpressErrorNotification() addDestinationTagRequiredNotification() + addDestinationBackupErrorNotification() maybeAddPriceImpactNotification() } @@ -134,6 +143,25 @@ internal class SwapNotificationsModel @Inject constructor( } } + private suspend fun MutableList.addDestinationBackupErrorNotification() { + val destinationAddress = notificationData.destinationAddress + if (destinationAddress.isEmpty()) return + + val problematicWalletId = resolveBackupProblematicWallet(destinationAddress) ?: return + add( + NotificationUM.Error.DestinationBackupError( + onContactSupport = { modelScope.launch { sendBackupProblemEmailUseCase(problematicWalletId) } }, + ), + ) + } + + private suspend fun resolveBackupProblematicWallet(address: String): UserWalletId? { + backupProblematicWalletCache.get()?.let { if (it.first == address) return it.second } + + return getBackupProblematicWalletForAddressUseCase(address) + .also { backupProblematicWalletCache.set(address to it) } + } + private fun MutableList.addInsufficientFundsNotification() { val enteredFromAmount = notificationData.enteredFromAmount ?: return val balance = notificationData.fromCryptoCurrencyStatus?.value?.amount ?: return diff --git a/features/swap/impl/build.gradle.kts b/features/swap/impl/build.gradle.kts index c8c67e303c..f955ca80e0 100644 --- a/features/swap/impl/build.gradle.kts +++ b/features/swap/impl/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation(projects.domain.express.models) implementation(projects.domain.account) implementation(projects.domain.account.status) + implementation(projects.domain.card) implementation(projects.domain.visa) implementation(projects.domain.markets) implementation(projects.domain.swap) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 70b750fdd2..59a6fee746 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -35,6 +35,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction +import com.tangem.core.ui.message.dialog.Dialogs import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseBigDecimalOrNull import com.tangem.core.ui.utils.parseToBigDecimal @@ -42,6 +43,7 @@ import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase +import com.tangem.domain.card.IsWalletBackupProblematicUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase @@ -49,6 +51,7 @@ import com.tangem.domain.express.models.ExpressOperationType import com.tangem.domain.express.models.ProviderFilterType import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase +import com.tangem.domain.feedback.SendBackupProblemEmailUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType @@ -149,6 +152,8 @@ internal class SwapModel @Inject constructor( private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val shouldShowStoriesUseCase: ShouldShowStoriesUseCase, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, + private val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase, + private val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase, private val swapInteractor: SwapInteractor, private val swapTransferInteractor: SwapTransferInteractor, private val swapTransferStateBuilder: SwapTransferStateBuilder, @@ -472,6 +477,12 @@ internal class SwapModel @Inject constructor( val selectedCurrencyStatus = result.currency val selectedAccount = result.account.account + if (!isFromDirection && isWalletBackupProblematicUseCase(selectedUserWallet)) { + router.pop() + showBackupErrorAlert(selectedUserWallet.walletId) + return + } + val (fromSwapCurrencyStatus, toSwapCurrencyStatus) = if (isFromDirection) { SwapCurrencyStatus( userWallet = selectedUserWallet, @@ -1862,6 +1873,14 @@ internal class SwapModel @Inject constructor( ) } + private fun showBackupErrorAlert(userWalletId: UserWalletId) { + messageSender.send( + Dialogs.backupErrorAddFundsDisabled( + onContactSupport = { modelScope.launch { sendBackupProblemEmailUseCase(userWalletId) } }, + ), + ) + } + private fun showTransactionErrorAlert( error: SwapTransactionState.Error, onSupportClick: (String) -> Unit = ::onFailedTxEmailClick, diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapModelTestBase.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapModelTestBase.kt index bcb0613fe9..6a80346c00 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapModelTestBase.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapModelTestBase.kt @@ -16,8 +16,10 @@ import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSy import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.card.IsWalletBackupProblematicUseCase import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase +import com.tangem.domain.feedback.SendBackupProblemEmailUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -101,6 +103,8 @@ internal abstract class SwapModelTestBase { protected val getSwapUiModeUseCase: GetSwapUiModeUseCase = mockk(relaxed = true) protected val setSwapUiModeUseCase: SetSwapUiModeUseCase = mockk(relaxed = true) protected val calculateAmountUseCase: CalculateAmountUseCase = mockk(relaxed = true) + protected val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase = mockk(relaxed = true) + protected val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase = mockk(relaxed = true) private val chooseTokenBridgeFactory: ChooseTokenBridge.Factory = mockk(relaxed = true) private val getUserCountryUseCase: GetUserCountryUseCase = mockk(relaxed = true) @@ -171,6 +175,8 @@ internal abstract class SwapModelTestBase { getSwapUiModeUseCase = getSwapUiModeUseCase, setSwapUiModeUseCase = setSwapUiModeUseCase, calculateAmountUseCase = calculateAmountUseCase, + isWalletBackupProblematicUseCase = isWalletBackupProblematicUseCase, + sendBackupProblemEmailUseCase = sendBackupProblemEmailUseCase, ) // region builders diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 7e2dcc93f9..a2bcd8d884 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -68,6 +68,7 @@ dependencies { implementation(projects.domain.demo) implementation(projects.domain.dynamicAddresses) implementation(projects.domain.dynamicAddresses.models) + implementation(projects.domain.feedback) implementation(projects.domain.markets.models) implementation(projects.domain.models) implementation(projects.domain.notifications.models) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsDialogFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsDialogFactory.kt index df70f292b1..599a643e50 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsDialogFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsDialogFactory.kt @@ -8,6 +8,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction +import com.tangem.core.ui.message.dialog.Dialogs import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.tokendetails.impl.R import javax.inject.Inject @@ -70,4 +71,8 @@ internal class TokenDetailsDialogFactory @Inject constructor( fun showError(text: TextReference) { uiMessageSender.send(DialogMessage(message = text)) } + + fun showBackupError(onContactSupport: () -> Unit) { + uiMessageSender.send(Dialogs.backupErrorAddFundsDisabled(onContactSupport = onContactSupport)) + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 47d675cd6b..59d3e11309 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -53,6 +53,8 @@ import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.card.IsWalletBackupProblematicUseCase +import com.tangem.domain.feedback.SendBackupProblemEmailUseCase import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.dynamicaddresses.DynamicAddressesSupportedBlockchains import com.tangem.domain.models.StatusSource @@ -159,6 +161,8 @@ internal class TokenDetailsModel @Inject constructor( private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase, private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, + private val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase, + private val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase, private val associateAssetUseCase: AssociateAssetUseCase, private val retryIncompleteTransactionUseCase: RetryIncompleteTransactionUseCase, private val openTrustlineUseCase: OpenTrustlineUseCase, @@ -588,6 +592,7 @@ internal class TokenDetailsModel @Inject constructor( if (handleUnavailabilityReason(unavailabilityReason = unavailabilityReason)) { return } + if (isTopUpBlockedByBackupError()) return val status = cryptoCurrencyStatus ?: return modelScope.launch { @@ -710,6 +715,7 @@ internal class TokenDetailsModel @Inject constructor( if (handleUnavailabilityReason(unavailabilityReason = unavailabilityReason)) { return } + if (isTopUpBlockedByBackupError()) return modelScope.launch { if (needShowYieldSupplyWarning()) { @@ -824,6 +830,7 @@ internal class TokenDetailsModel @Inject constructor( if (handleUnavailabilityReason(unavailabilityReason = unavailabilityReason)) { return } + if (isTopUpBlockedByBackupError()) return modelScope.launch { if (checkYieldSupply && needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus)) { @@ -1199,6 +1206,19 @@ internal class TokenDetailsModel @Inject constructor( return true } + private fun isTopUpBlockedByBackupError(): Boolean { + if (!isWalletBackupProblematicUseCase(userWallet)) return false + + dialogFactory.showBackupError(onContactSupport = ::contactBackupSupport) + return true + } + + private fun contactBackupSupport() { + modelScope.launch { + sendBackupProblemEmailUseCase(userWallet.walletId) + } + } + private fun openStaking() { modelScope.launch { getStakingAvailabilityUseCase.invokeSync(userWalletId, cryptoCurrency) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt index f05035e3ad..1b19f874c0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt @@ -11,6 +11,7 @@ import com.tangem.common.ui.tokens.getUnavailabilityReasonText import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic.ButtonSupport import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped @@ -27,10 +28,12 @@ import com.tangem.domain.account.status.usecase.IsCryptoCurrencyCouldHideUseCase import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap +import com.tangem.domain.card.IsWalletBackupProblematicUseCase import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.utils.lceError import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.feedback.SendBackupProblemEmailUseCase import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.account.AccountId @@ -58,6 +61,7 @@ import com.tangem.domain.transaction.usecase.ReceiveAddressesFactory import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.domain.unwrap import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController @@ -144,6 +148,9 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val isCryptoCurrencyCouldHideUseCase: IsCryptoCurrencyCouldHideUseCase, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val uiMessageSender: UiMessageSender, + private val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase, ) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { override fun onSendClick( @@ -196,6 +203,8 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( event?.let { analyticsEventHandler.send(it) } + if (isTopUpBlockedByBackupError(accountId.userWalletId)) return + modelScope.launch(dispatchers.main) { if (needShowYieldSupplyWarning(cryptoCurrencyStatus)) { stateHolder.hideBottomSheet() @@ -339,6 +348,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( ) if (handleUnavailabilityReason(unavailabilityReason)) return + if (isTopUpBlockedByBackupError(accountId.userWalletId)) return appRouter.push( AppRoute.Onramp( @@ -363,6 +373,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( ) if (handleUnavailabilityReason(unavailabilityReason)) return + if (isTopUpBlockedByBackupError(accountId.userWalletId)) return modelScope.launch(dispatchers.main) { if (needShowYieldSupplyWarning(cryptoCurrencyStatus)) { @@ -451,6 +462,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } override fun onMultiWalletSwapClick(userWalletId: UserWalletId) { + if (isTopUpBlockedByBackupError(userWalletId)) return if (!isMultiWalletTokensLoaded()) return modelScope.launch { @@ -469,6 +481,8 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } override fun onMultiWalletBuyClick(userWalletId: UserWalletId, screenType: String) { + if (isTopUpBlockedByBackupError(userWalletId)) return + onMultiWalletActionClick( statusFlow = rampStateManager.getExpressInitializationStatus(userWalletId), route = AppRoute.BuyCrypto(userWalletId = userWalletId), @@ -565,6 +579,26 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( return true } + private fun isTopUpBlockedByBackupError(userWalletId: UserWalletId): Boolean { + val userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return false + if (!isWalletBackupProblematicUseCase(userWallet)) return false + + stateHolder.hideBottomSheet() + uiMessageSender.send( + WalletAlertUM.addFundsDisabledForBackupError( + onContactSupport = { contactBackupSupport(userWallet) }, + ), + ) + return true + } + + private fun contactBackupSupport(userWallet: UserWallet) { + analyticsEventHandler.send(ButtonSupport(source = AnalyticsParam.ScreensSources.Main)) + modelScope.launch { + sendBackupProblemEmailUseCase(userWallet.walletId) + } + } + private fun isMultiWalletTokensLoaded(): Boolean { return if (stateHolder.value.isRedesignEnabled) { val selectedWalletUM = stateHolder.getSelectedWalletUM() as? WalletUM.Content ?: return false diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt deleted file mode 100644 index 3f31e4730b..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.domain - -import com.tangem.common.card.EllipticCurve -import com.tangem.domain.card.configs.CardConfig -import com.tangem.domain.models.scan.CardDTO -import javax.inject.Inject - -class BackupValidator @Inject constructor() { - - fun isValidFull(cardDTO: CardDTO): Boolean { - return validateBackupStatus(cardDTO) && validateCurves(cardDTO) - } - - fun isValidBackupStatus(cardDTO: CardDTO): Boolean { - return validateBackupStatus(cardDTO) - } - - private fun validateCurves(cardDTO: CardDTO): Boolean { - val config = CardConfig.createConfig(cardDTO) - // / Since the curve `bls12381_G2_AUG` was added later into first generation of wallets, - // we cannot determine whether this curve is missing due to an error or because the user - // did not want to recreate the wallet. - val expectedCurves = config.mandatoryCurves - .filterNot { it == EllipticCurve.Bls12381G2Aug } - val curves = cardDTO.wallets.map { it.curve } - for (expectedCurve in expectedCurves) { - val cardCurvesCount = curves.count { it == expectedCurve } - - // missing curve - if (cardCurvesCount == 0) { - return false - } - - // duplicated curve - if (cardCurvesCount > 1) { - return false - } - } - return true - } - - private fun validateBackupStatus(cardDTO: CardDTO): Boolean { - val backupStatus = cardDTO.backupStatus - backupStatus ?: return true // for card with null backup status, validation should always returns true - return backupStatus !is CardDTO.BackupStatus.CardLinked - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index c9f1c94895..9aa1f38290 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -13,6 +13,7 @@ import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress import com.tangem.domain.assetsdiscovery.usecase.ObserveAssetsDiscoveryUseCase import com.tangem.domain.card.CardTypesResolver +import com.tangem.domain.card.IsWalletBackupProblematicUseCase import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase @@ -56,7 +57,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private val isDemoCardUseCase: IsDemoCardUseCase, private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, private val isNeedToBackupUseCase: IsNeedToBackupUseCase, - private val backupValidator: BackupValidator, + private val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase, private val notificationsRepository: NotificationsRepository, private val accountDependencies: AccountDependencies, private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase, @@ -118,6 +119,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val isAddFundsBannerShown = isAddFundsBannerVisible(accountStatusList.totalFiatBalance) buildList { + addBackupErrorNotification(userWallet, clickIntents) + addUsedOutdatedDataNotification(accountStatusList.totalFiatBalance) addAddFundsBanner( @@ -126,7 +129,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( clickIntents = clickIntents, ) - addCriticalNotifications(userWallet, clickIntents) + addCriticalNotifications(userWallet) addUpgradeHotWalletPromoNotification( userWallet = userWallet, @@ -270,20 +273,22 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - private fun MutableList.addCriticalNotifications( + private fun MutableList.addBackupErrorNotification( userWallet: UserWallet, clickIntents: WalletClickIntents, ) { + addIf( + element = WalletNotification.Critical.BackupError { clickIntents.onBackupErrorClick() }, + condition = isWalletBackupProblematicUseCase(userWallet), + ) + } + + private fun MutableList.addCriticalNotifications(userWallet: UserWallet) { if (userWallet !is UserWallet.Cold) { return } val cardTypesResolver = userWallet.scanResponse.cardTypesResolver - addIf( - element = WalletNotification.Critical.BackupError { clickIntents.onBackupErrorClick() }, - condition = !backupValidator.isValidBackupStatus(userWallet.scanResponse.card) || userWallet.hasBackupError, - ) - addIf( element = WalletNotification.Critical.DevCard, condition = !cardTypesResolver.isReleaseFirmwareType(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt index f834a58d30..e6afe6006c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt @@ -8,6 +8,7 @@ import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress import com.tangem.domain.assetsdiscovery.usecase.ObserveAssetsDiscoveryUseCase import com.tangem.domain.card.CardTypesResolver +import com.tangem.domain.card.IsWalletBackupProblematicUseCase import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase @@ -44,7 +45,7 @@ import javax.inject.Inject internal class GetWalletNotificationsFactory @Inject constructor( private val isDemoCardUseCase: IsDemoCardUseCase, private val isNeedToBackupUseCase: IsNeedToBackupUseCase, - private val backupValidator: BackupValidator, + private val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase, private val accountDependencies: AccountDependencies, private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase, private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase, @@ -167,7 +168,7 @@ internal class GetWalletNotificationsFactory @Inject constructor( val cardTypesResolver = userWallet.scanResponse.cardTypesResolver addIf( element = WalletNotificationUM.BackupError { clickIntents.onSupportClick() }, - condition = !backupValidator.isValidBackupStatus(userWallet.scanResponse.card) || userWallet.hasBackupError, + condition = isWalletBackupProblematicUseCase(userWallet), ) addIf( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertUM.kt index ac56278723..5257bd47c5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertUM.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertUM.kt @@ -4,6 +4,7 @@ import com.tangem.core.ui.extensions.WrappedList import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction +import com.tangem.core.ui.message.dialog.Dialogs import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.feature.wallet.impl.R @@ -55,6 +56,10 @@ internal object WalletAlertUM { ) } + fun addFundsDisabledForBackupError(onContactSupport: () -> Unit): DialogMessage { + return Dialogs.backupErrorAddFundsDisabled(onContactSupport = onContactSupport) + } + fun insufficientTokensCountForSwapping(): DialogMessage { return DialogMessage( title = resourceReference(R.string.action_buttons_swap_no_tokens_added_alert_title), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index fca10e657d..795d9c48d9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -50,9 +50,9 @@ sealed class WalletNotification(val config: NotificationConfig) { ) data class BackupError(val onSupportClick: () -> Unit) : Critical( - title = resourceReference(R.string.warning_backup_errors_title), - subtitle = resourceReference(R.string.warning_backup_errors_message), - buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + title = resourceReference(R.string.warning_backup_error_attention_title), + subtitle = resourceReference(R.string.warning_backup_error_attention_message), + buttonsState = ButtonsState.PrimaryButtonConfig( text = resourceReference(id = R.string.common_contact_support), onClick = onSupportClick, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index 765a96d132..7959eab220 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -17,6 +17,7 @@ import com.tangem.core.ui.extensions.annotatedReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.ForceDarkTheme +import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.test.WalletNotificationTestTags import com.tangem.feature.wallet.impl.R @@ -66,6 +67,16 @@ internal fun LazyListScope.notifications(configs: ImmutableList { + Notification( + config = item.config, + modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null), + containerColor = TangemColorPalette.Amaranth_30, + borderColor = TangemColorPalette.Amaranth, + iconTint = TangemTheme.colors.icon.warning, + subtitleColor = TangemTheme.colors.text.primary1, + ) + } is WalletNotification.CreateTangemPayAccount -> { CreatePaymentAccountNotification( modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),