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 cef5171d26..9477d6c380 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 @@ -55,6 +55,18 @@ internal object CardDomainModule { return IsNeedToBackupUseCase(userWalletsListRepository = userWalletsListRepository) } + @Provides + @Singleton + fun provideHasSingleWalletSignedHashesUseCase( + cardRepository: CardRepository, + walletManagersFacade: WalletManagersFacade, + ): HasSingleWalletSignedHashesUseCase { + return HasSingleWalletSignedHashesUseCase( + cardRepository = cardRepository, + walletManagersFacade = walletManagersFacade, + ) + } + @Provides @Singleton fun provideGetExtendedPublicKeyForCurrencyUseCase( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/HasSingleWalletSignedHashesUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/HasSingleWalletSignedHashesUseCase.kt similarity index 86% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/HasSingleWalletSignedHashesUseCase.kt rename to domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/HasSingleWalletSignedHashesUseCase.kt index 4f32140696..be6a4b0ddd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/HasSingleWalletSignedHashesUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/HasSingleWalletSignedHashesUseCase.kt @@ -1,6 +1,5 @@ -package com.tangem.feature.wallet.presentation.wallet.domain +package com.tangem.domain.wallets.usecase -import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.demo.models.DemoConfig @@ -10,10 +9,8 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import javax.inject.Inject -@ModelScoped -class HasSingleWalletSignedHashesUseCase @Inject constructor( +class HasSingleWalletSignedHashesUseCase( private val cardRepository: CardRepository, private val walletManagersFacade: WalletManagersFacade, ) { @@ -28,11 +25,16 @@ class HasSingleWalletSignedHashesUseCase @Inject constructor( return@map false } + val signedHashes = userWallet.scanResponse.card.wallets + .firstOrNull() + ?.totalSignedHashes + ?: 0 + return@map try { walletManagersFacade.validateSignatureCount( userWalletId = userWallet.walletId, network = network, - signedHashes = userWallet.scanResponse.card.wallets.firstOrNull()?.totalSignedHashes ?: 0, + signedHashes = signedHashes, ) .fold( ifLeft = { true }, diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index a2bcd8d884..6a8211f32c 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -69,6 +69,7 @@ dependencies { implementation(projects.domain.dynamicAddresses) implementation(projects.domain.dynamicAddresses.models) implementation(projects.domain.feedback) + implementation(projects.domain.feedback.models) 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/domain/GetWalletCardWarningsUseCase.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/domain/GetWalletCardWarningsUseCase.kt new file mode 100644 index 0000000000..97f5e2c457 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/domain/GetWalletCardWarningsUseCase.kt @@ -0,0 +1,77 @@ +package com.tangem.feature.tokendetails.domain + +import com.tangem.domain.card.IsWalletBackupProblematicUseCase +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.HasSingleWalletSignedHashesUseCase +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +/** + * Produces card/wallet-level warnings for the Token Details screen. + * + * These banners mirror the ones the main screen shows via `GetWalletNotificationsFactory`, but they only apply to + * a single-currency cold wallet (a multi-currency wallet keeps them on the main screen only). + */ +internal class GetWalletCardWarningsUseCase @Inject constructor( + private val isDemoCardUseCase: IsDemoCardUseCase, + private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase, + private val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase, + private val dispatchers: CoroutineDispatcherProvider, +) { + + operator fun invoke(userWallet: UserWallet, network: Network): Flow> { + if (userWallet !is UserWallet.Cold || userWallet.isMultiCurrency) { + return flowOf(emptySet()) + } + + return hasSingleWalletSignedHashesUseCase(userWallet, network) + .map { hasIncorrectSignedHashes -> + buildWarnings( + userWallet = userWallet, + hasIncorrectSignedHashes = hasIncorrectSignedHashes, + ) + } + .flowOn(dispatchers.io) + } + + private fun buildWarnings(userWallet: UserWallet.Cold, hasIncorrectSignedHashes: Boolean): Set { + val cardTypesResolver = userWallet.cardTypesResolver + + return buildSet { + if (isWalletBackupProblematicUseCase(userWallet)) { + add(WalletCardWarning.BackupError) + } + if (!cardTypesResolver.isReleaseFirmwareType()) { + add(WalletCardWarning.DevCard) + } + if (cardTypesResolver.isReleaseFirmwareType() && cardTypesResolver.isAttestationFailed()) { + add(WalletCardWarning.FailedCardValidation) + } + cardTypesResolver.getRemainingSignatures()?.let { remainingSignatures -> + if (remainingSignatures <= MAX_REMAINING_SIGNATURES_COUNT) { + add(WalletCardWarning.LowSignatures(count = remainingSignatures)) + } + } + if (isDemoCardUseCase(cardId = cardTypesResolver.getCardId())) { + add(WalletCardWarning.DemoCard) + } + if (cardTypesResolver.isTestCard()) { + add(WalletCardWarning.TestnetCard) + } + if (hasIncorrectSignedHashes) { + add(WalletCardWarning.NumberOfSignedHashesIncorrect) + } + } + } + + private companion object { + const val MAX_REMAINING_SIGNATURES_COUNT = 10 + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/domain/WalletCardWarning.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/domain/WalletCardWarning.kt new file mode 100644 index 0000000000..afc9f50393 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/domain/WalletCardWarning.kt @@ -0,0 +1,24 @@ +package com.tangem.feature.tokendetails.domain + +/** + * Card/wallet-level warnings that are shown on the main screen for a single-currency wallet and must also be + * displayed in Token Details (redesign), since single-currency wallets now have a Token Details screen. + * + * Currency-level warnings are produced separately by [GetCurrencyWarningsUseCase]. + */ +internal sealed interface WalletCardWarning { + + data object BackupError : WalletCardWarning + + data object DevCard : WalletCardWarning + + data object FailedCardValidation : WalletCardWarning + + data object TestnetCard : WalletCardWarning + + data class LowSignatures(val count: Int) : WalletCardWarning + + data object NumberOfSignedHashesIncorrect : WalletCardWarning + + data object DemoCard : WalletCardWarning +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt index d2e32e2a48..6ae77ff99c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt @@ -76,6 +76,14 @@ interface TokenDetailsClickIntents { fun onYieldInfoClick() + // region Wallet card warnings + + fun onSupportClick() + + fun onCloseSignedHashesWarning() + + // endregion Wallet card warnings + // region Clore migration // TODO: Remove after 2025-04-01 when Clore migration ends ([REDACTED_TASK_KEY]) @@ -157,6 +165,10 @@ internal class EmptyTokenDetailsClickIntents : TokenDetailsClickIntents { override fun onCloseRentInfoNotification() { /* no op */ } + override fun onSupportClick() { /* no op */ } + + override fun onCloseSignedHashesWarning() { /* no op */ } + override fun onRetryIncompleteTransactionClick() { /* no op */ } override fun onOpenTrustlineClick() { /* no op */ } 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 59d3e11309..2e1bbdb28f 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 @@ -63,6 +63,10 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkAddress +import com.tangem.domain.card.SetCardWasScannedUseCase +import com.tangem.domain.feedback.GetWalletMetaInfoUseCase +import com.tangem.domain.feedback.SendFeedbackEmailUseCase +import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.offramp.GetOfframpUrlUseCase @@ -98,6 +102,7 @@ import com.tangem.domain.yield.supply.models.YieldSupplyRewardBalance import com.tangem.domain.yield.supply.usecase.YieldSupplyGetRewardsBalanceUseCase import com.tangem.feature.tokendetails.deeplink.TokenDetailsDeepLinkActionListener import com.tangem.feature.tokendetails.domain.GetCurrencyWarningsUseCase +import com.tangem.feature.tokendetails.domain.GetWalletCardWarningsUseCase import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsCurrencyStatusAnalyticsSender import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsNotificationsAnalyticsSender @@ -123,6 +128,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.transform import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.ToggleBalanceTypeTransformer import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.UpdateStakingNotificationTransformer import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.UpdateNotificationsTransformer +import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.UpdateWalletCardWarningsTransformer import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.UpdateTopBarMenuTransformer import com.tangem.features.tokendetails.ExpressTransactionsEvent import com.tangem.features.tokendetails.ExpressTransactionsEventListener @@ -155,6 +161,10 @@ internal class TokenDetailsModel @Inject constructor( private val isCryptoCurrencyCouldHideUseCase: IsCryptoCurrencyCouldHideUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase, + private val getWalletCardWarningsUseCase: GetWalletCardWarningsUseCase, + private val setCardWasScannedUseCase: SetCardWasScannedUseCase, + private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, + private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val getExtendedPublicKeyForCurrencyUseCase: GetExtendedPublicKeyForCurrencyUseCase, private val getStakingEntryInfoUseCase: GetStakingEntryInfoUseCase, @@ -423,13 +433,20 @@ internal class TokenDetailsModel @Inject constructor( private fun updateWarnings(cryptoCurrencyStatus: CryptoCurrencyStatus) { modelScope.launch(dispatchers.main) { - getCurrencyWarningsUseCase( - userWalletId = userWalletId, - currencyStatus = cryptoCurrencyStatus, - derivationPath = cryptoCurrency.network.derivationPath, + combine( + flow = getCurrencyWarningsUseCase( + userWalletId = userWalletId, + currencyStatus = cryptoCurrencyStatus, + derivationPath = cryptoCurrency.network.derivationPath, + ), + flow2 = getWalletCardWarningsUseCase( + userWallet = userWallet, + network = cryptoCurrency.network, + ), + transform = ::Pair, ) .distinctUntilChanged() - .onEach { warnings -> + .onEach { (warnings, cardWarnings) -> val updatedState = stateFactory.getStateWithNotifications(warnings) notificationsAnalyticsSender.send(uiState.value, updatedState.notifications) uiState.value = updatedState @@ -440,6 +457,12 @@ internal class TokenDetailsModel @Inject constructor( clickIntents = this@TokenDetailsModel, ), ) + redesignStateController.update( + UpdateWalletCardWarningsTransformer( + walletCardWarnings = cardWarnings, + clickIntents = this@TokenDetailsModel, + ), + ) } .launchIn(modelScope) .saveIn(warningsJobHolder) @@ -986,6 +1009,21 @@ internal class TokenDetailsModel @Inject constructor( uiState.value = stateFactory.getStateWithRemovedRentNotification() } + override fun onSupportClick() { + modelScope.launch { + val metaInfo = getWalletMetaInfoUseCase(userWalletId).getOrNull() ?: return@launch + sendFeedbackEmailUseCase(type = FeedbackEmailType.DirectUserRequest(walletMetaInfo = metaInfo)) + } + } + + override fun onCloseSignedHashesWarning() { + modelScope.launch { + (userWallet as? UserWallet.Cold)?.let { coldWallet -> + setCardWasScannedUseCase(cardId = coldWallet.cardId) + } + } + } + override fun onCopyAddress(): TextReference? { val networkAddress = cryptoCurrencyStatus?.value?.networkAddress ?: return null val addresses = networkAddress.availableAddresses.mapToAddressModels(cryptoCurrency).toImmutableList() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateWalletCardWarningsTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateWalletCardWarningsTransformer.kt new file mode 100644 index 0000000000..2f3bc9bc7e --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateWalletCardWarningsTransformer.kt @@ -0,0 +1,131 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.tangem.core.ui.R +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.message.TangemMessageButtonUM +import com.tangem.core.ui.ds.message.TangemMessageEffect +import com.tangem.core.ui.ds.message.TangemMessageUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tokendetails.domain.WalletCardWarning +import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import com.tangem.core.res.R as CoreResR + +/** + * Maps card/wallet-level warnings (produced by `GetWalletCardWarningsUseCase` for a single-currency wallet) into + * Token Details notifications and prepends them to the existing currency-level notifications. + */ +internal class UpdateWalletCardWarningsTransformer( + private val walletCardWarnings: Set, + private val clickIntents: TokenDetailsClickIntents, +) : Transformer { + + override fun transform(prevState: TokenDetailsUM): TokenDetailsUM { + val others = prevState.notifications.filterNot { it.id in WALLET_CARD_WARNING_IDS } + val messages = walletCardWarnings.map(::mapWarning) + + return prevState.copy( + notifications = (messages + others).toImmutableList(), + ) + } + + @Suppress("LongMethod") + private fun mapWarning(warning: WalletCardWarning): TangemMessageUM { + return when (warning) { + WalletCardWarning.BackupError -> TangemMessageUM( + id = ID_BACKUP_ERROR, + title = resourceReference(CoreResR.string.warning_backup_errors_title), + subtitle = resourceReference(CoreResR.string.warning_backup_errors_message), + messageEffect = TangemMessageEffect.Warning, + iconUM = attentionIcon(), + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(CoreResR.string.common_contact_support), + type = TangemButtonType.Secondary, + onClick = clickIntents::onSupportClick, + ), + ), + ) + WalletCardWarning.DevCard -> TangemMessageUM( + id = ID_DEV_CARD, + title = resourceReference(CoreResR.string.warning_developer_card_title), + subtitle = resourceReference(CoreResR.string.warning_developer_card_message), + messageEffect = TangemMessageEffect.None, + iconUM = attentionIcon(), + ) + WalletCardWarning.FailedCardValidation -> TangemMessageUM( + id = ID_FAILED_CARD_VALIDATION, + title = resourceReference(CoreResR.string.warning_failed_to_verify_card_title), + subtitle = resourceReference(CoreResR.string.warning_failed_to_verify_card_message), + messageEffect = TangemMessageEffect.Warning, + iconUM = attentionIcon(), + ) + WalletCardWarning.TestnetCard -> TangemMessageUM( + id = ID_TESTNET_CARD, + title = resourceReference(CoreResR.string.warning_testnet_card_title), + subtitle = resourceReference(CoreResR.string.warning_testnet_card_message), + messageEffect = TangemMessageEffect.None, + iconUM = attentionIcon(), + ) + is WalletCardWarning.LowSignatures -> TangemMessageUM( + id = ID_LOW_SIGNATURES, + title = resourceReference(CoreResR.string.warning_low_signatures_title), + subtitle = resourceReference( + id = CoreResR.string.warning_low_signatures_message, + formatArgs = wrappedList(warning.count.toString()), + ), + messageEffect = TangemMessageEffect.None, + iconUM = attentionIcon(), + ) + WalletCardWarning.NumberOfSignedHashesIncorrect -> TangemMessageUM( + id = ID_NUMBER_OF_SIGNED_HASHES_INCORRECT, + title = resourceReference(CoreResR.string.warning_number_of_signed_hashes_incorrect_title), + subtitle = resourceReference(CoreResR.string.warning_number_of_signed_hashes_incorrect_message), + messageEffect = TangemMessageEffect.Warning, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.img_knight_shield_32, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + onCloseClick = clickIntents::onCloseSignedHashesWarning, + ) + WalletCardWarning.DemoCard -> TangemMessageUM( + id = ID_DEMO_CARD, + title = resourceReference(CoreResR.string.warning_demo_mode_title), + subtitle = resourceReference(CoreResR.string.warning_demo_mode_message), + messageEffect = TangemMessageEffect.None, + iconUM = attentionIcon(), + ) + } + } + + private fun attentionIcon(): TangemIconUM.Icon = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ) + + private companion object { + const val ID_BACKUP_ERROR = "BackupErrorNotification" + const val ID_DEV_CARD = "DevCardNotification" + const val ID_FAILED_CARD_VALIDATION = "FailedCardValidationNotification" + const val ID_TESTNET_CARD = "TestnetCardNotification" + const val ID_LOW_SIGNATURES = "LowSignaturesNotification" + const val ID_NUMBER_OF_SIGNED_HASHES_INCORRECT = "NumberOfSignedHashesIncorrectNotification" + const val ID_DEMO_CARD = "DemoCardNotification" + + val WALLET_CARD_WARNING_IDS = setOf( + ID_BACKUP_ERROR, + ID_DEV_CARD, + ID_FAILED_CARD_VALIDATION, + ID_TESTNET_CARD, + ID_LOW_SIGNATURES, + ID_NUMBER_OF_SIGNED_HASHES_INCORRECT, + ID_DEMO_CARD, + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/domain/GetWalletCardWarningsUseCaseTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/domain/GetWalletCardWarningsUseCaseTest.kt new file mode 100644 index 0000000000..032327d98b --- /dev/null +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/domain/GetWalletCardWarningsUseCaseTest.kt @@ -0,0 +1,169 @@ +package com.tangem.feature.tokendetails.domain + +import com.google.common.truth.Truth.assertThat +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.models.network.Network +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.HasSingleWalletSignedHashesUseCase +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +internal class GetWalletCardWarningsUseCaseTest { + + private val isDemoCardUseCase: IsDemoCardUseCase = mockk() + private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase = mockk() + private val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase = mockk() + private val network: Network = mockk(relaxed = true) + + private val useCase = GetWalletCardWarningsUseCase( + isDemoCardUseCase = isDemoCardUseCase, + hasSingleWalletSignedHashesUseCase = hasSingleWalletSignedHashesUseCase, + isWalletBackupProblematicUseCase = isWalletBackupProblematicUseCase, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @BeforeEach + fun setup() { + clearMocks( + isDemoCardUseCase, + hasSingleWalletSignedHashesUseCase, + isWalletBackupProblematicUseCase, + ) + mockkStatic(UserWallet.Cold::cardTypesResolver) + + // Default: nothing flagged + every { isDemoCardUseCase(any()) } returns false + every { hasSingleWalletSignedHashesUseCase(any(), any()) } returns flowOf(false) + every { isWalletBackupProblematicUseCase(any()) } returns false + } + + @AfterEach + fun tearDown() { + unmockkStatic(UserWallet.Cold::cardTypesResolver) + } + + @Test + fun `GIVEN multi-currency cold wallet WHEN invoke THEN empty set`() = runTest { + // Arrange + val wallet = coldWallet(isMultiCurrency = true) + + // Act + val result = useCase(userWallet = wallet, network = network).first() + + // Assert + assertThat(result).isEmpty() + } + + @Test + fun `GIVEN hot wallet WHEN invoke THEN empty set`() = runTest { + // Arrange + val wallet = mockk(relaxed = true) + + // Act + val result = useCase(userWallet = wallet, network = network).first() + + // Assert + assertThat(result).isEmpty() + } + + @Test + fun `GIVEN single-currency cold wallet with clean card WHEN invoke THEN empty set`() = runTest { + // Arrange + val wallet = coldWallet(isMultiCurrency = false, resolver = cleanResolver()) + + // Act + val result = useCase(userWallet = wallet, network = network).first() + + // Assert + assertThat(result).isEmpty() + } + + @Test + fun `GIVEN single-currency dev card WHEN invoke THEN only dev card warning`() = runTest { + // Arrange + val resolver = cleanResolver().also { every { it.isReleaseFirmwareType() } returns false } + val wallet = coldWallet(isMultiCurrency = false, resolver = resolver) + + // Act + val result = useCase(userWallet = wallet, network = network).first() + + // Assert + assertThat(result).containsExactly(WalletCardWarning.DevCard) + } + + @Test + fun `GIVEN single-currency release card with every condition WHEN invoke THEN full warning set`() = runTest { + // Arrange + val resolver = cleanResolver().also { + every { it.isReleaseFirmwareType() } returns true + every { it.isAttestationFailed() } returns true + every { it.getRemainingSignatures() } returns LOW_SIGNATURES + every { it.isTestCard() } returns true + } + val wallet = coldWallet(isMultiCurrency = false, resolver = resolver) + every { isWalletBackupProblematicUseCase(any()) } returns true + every { isDemoCardUseCase(any()) } returns true + every { hasSingleWalletSignedHashesUseCase(any(), any()) } returns flowOf(true) + + // Act + val result = useCase(userWallet = wallet, network = network).first() + + // Assert + assertThat(result).containsExactly( + WalletCardWarning.BackupError, + WalletCardWarning.FailedCardValidation, + WalletCardWarning.TestnetCard, + WalletCardWarning.LowSignatures(count = LOW_SIGNATURES), + WalletCardWarning.DemoCard, + WalletCardWarning.NumberOfSignedHashesIncorrect, + ) + } + + @Test + fun `GIVEN single-currency cold wallet with backup problem WHEN invoke THEN backup error warning`() = runTest { + // Arrange + val wallet = coldWallet(isMultiCurrency = false, resolver = cleanResolver()) + every { isWalletBackupProblematicUseCase(any()) } returns true + + // Act + val result = useCase(userWallet = wallet, network = network).first() + + // Assert + assertThat(result).containsExactly(WalletCardWarning.BackupError) + } + + private fun coldWallet( + isMultiCurrency: Boolean, + resolver: CardTypesResolver = cleanResolver(), + ): UserWallet.Cold { + return mockk(relaxed = true) { + every { this@mockk.isMultiCurrency } returns isMultiCurrency + every { cardTypesResolver } returns resolver + } + } + + private fun cleanResolver(): CardTypesResolver = mockk { + every { isReleaseFirmwareType() } returns true + every { isAttestationFailed() } returns false + every { getRemainingSignatures() } returns null + every { isTestCard() } returns false + every { getCardId() } returns "card" + } + + private companion object { + const val LOW_SIGNATURES = 5 + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt index 82596dbd03..b31d3c96a2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt @@ -16,6 +16,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.settings.IsReadyToShowRateAppUseCase import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.domain.wallets.usecase.HasSingleWalletSignedHashesUseCase import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification 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 38305069d8..6c689932b7 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 @@ -20,6 +20,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.domain.wallets.usecase.HasSingleWalletSignedHashesUseCase import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.impl.R