diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt index 9cc41144ee..735192a0ff 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt @@ -10,6 +10,7 @@ import com.tangem.core.ui.extensions.themedColor import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.generated.icons.Icons import com.tangem.core.ui.res.generated.icons.ic_document_20 +import com.tangem.domain.models.StatusSource import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.pay.TangemPayCard import com.tangem.domain.models.pay.TangemPayCardFrozenState @@ -59,9 +60,11 @@ internal class TangemPayDetailsStateFactory( } fun getLoadedState(status: PaymentAccountStatusValue.Loaded): TangemPayDetailsUM { + val isFresh = status.source == StatusSource.ACTUAL && status.error == null val hasUnfrozenCard = status.cards.any { it.frozenState == TangemPayCardFrozenState.Unfrozen } val hasIssuingCard = status.cards.any { it.state == TangemPayCardState.Issuing } - val isAddCardEnabled = status.error == null && !hasIssuingCard + val isAddCardEnabled = isFresh && !hasIssuingCard + val areActionButtonsEnabled = isFresh && hasUnfrozenCard return TangemPayDetailsUM( topBarConfig = TangemPayDetailsTopBarConfig( onBackClick = onBack, @@ -74,7 +77,7 @@ internal class TangemPayDetailsStateFactory( onRefresh = intents::onRefreshSwipe, ), balanceBlockState = TangemPayDetailsBalanceBlockState.Loading( - actionButtons = getActionButtonsConfig(isEnabled = status.error == null && hasUnfrozenCard), + actionButtons = getActionButtonsConfig(isEnabled = areActionButtonsEnabled), cardsBlockState = TangemPayDetailsBalanceBlockState.CardsBlockState( cards = status.cards .let { if (isMultipleCardsEnabled) it else it.take(1) } diff --git a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactoryTest.kt b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactoryTest.kt new file mode 100644 index 0000000000..78e8ef491e --- /dev/null +++ b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactoryTest.kt @@ -0,0 +1,142 @@ +package com.tangem.features.tangempay.entity + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.account.PaymentAccountStatusValue +import com.tangem.domain.models.pay.TangemPayCard +import com.tangem.domain.models.pay.TangemPayCardFrozenState +import com.tangem.domain.models.pay.TangemPayCardState +import com.tangem.features.tangempay.utils.TangemPayDetailIntents +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.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource + +internal class TangemPayDetailsStateFactoryTest { + + private val intents: TangemPayDetailIntents = mockk(relaxed = true) + + @BeforeEach + fun resetMocks() { + clearMocks(intents) + } + + private val activeUnfrozenCard = TangemPayCard( + id = "card_1", + productInstanceId = "pi_card_1", + cardStatus = TangemPayCard.Status.ACTIVE, + hasPinCode = false, + displayName = null, + frozenState = TangemPayCardFrozenState.Unfrozen, + lastDigits = "1234", + limit = null, + state = TangemPayCardState.Active, + ) + + private fun createFactory() = TangemPayDetailsStateFactory( + onBack = {}, + onOpenMenu = {}, + intents = intents, + isRedesignEnabled = true, + isRemoveAccountEnabled = true, + isMultipleCardsEnabled = true, + ) + + private fun loadedStatus( + statusSource: StatusSource, + statusError: PaymentAccountStatusValue.Error?, + statusCards: List = listOf(activeUnfrozenCard), + ): PaymentAccountStatusValue.Loaded = mockk(relaxed = true) { + every { source } returns statusSource + every { error } returns statusError + every { cards } returns statusCards + } + + @ParameterizedTest + @MethodSource("provideButtonStateCases") + fun `GIVEN status source WHEN getLoadedState THEN buttons enabled only for actual data without error`( + case: ButtonStateCase, + ) { + // Arrange + val factory = createFactory() + val status = loadedStatus(statusSource = case.source, statusError = case.error) + + // Act + val state = factory.getLoadedState(status) + + // Assert + val actionButtonsEnabled = state.balanceBlockState.actionButtons.map { it.isEnabled } + assertThat(actionButtonsEnabled).containsExactly(case.expectedEnabled, case.expectedEnabled) + assertThat(state.balanceBlockState.cardsBlockState?.isAddCardEnabled).isEqualTo(case.expectedEnabled) + } + + @Test + fun `GIVEN actual status with only frozen card WHEN getLoadedState THEN action buttons disabled`() { + // Arrange + val factory = createFactory() + val frozenCard = activeUnfrozenCard.copy(frozenState = TangemPayCardFrozenState.Frozen) + val status = loadedStatus( + statusSource = StatusSource.ACTUAL, + statusError = null, + statusCards = listOf(frozenCard), + ) + + // Act + val state = factory.getLoadedState(status) + + // Assert + assertThat(state.balanceBlockState.actionButtons.map { it.isEnabled }).containsExactly(false, false) + assertThat(state.balanceBlockState.cardsBlockState?.isAddCardEnabled).isTrue() + } + + @Test + fun `GIVEN actual status with issuing card WHEN getLoadedState THEN add card disabled`() { + // Arrange + val factory = createFactory() + val issuingCard = activeUnfrozenCard.copy(state = TangemPayCardState.Issuing) + val status = loadedStatus( + statusSource = StatusSource.ACTUAL, + statusError = null, + statusCards = listOf(activeUnfrozenCard, issuingCard), + ) + + // Act + val state = factory.getLoadedState(status) + + // Assert + assertThat(state.balanceBlockState.cardsBlockState?.isAddCardEnabled).isFalse() + } + + internal data class ButtonStateCase( + val source: StatusSource, + val error: PaymentAccountStatusValue.Error?, + val expectedEnabled: Boolean, + ) + + private companion object { + @JvmStatic + fun provideButtonStateCases() = listOf( + // Fresh data from the network -> actions allowed. + ButtonStateCase(source = StatusSource.ACTUAL, error = null, expectedEnabled = true), + // Cache restored from disk before a refresh confirms it -> actions blocked. + ButtonStateCase(source = StatusSource.CACHE, error = null, expectedEnabled = false), + // Internet unavailable, only cache left ([REDACTED_TASK_KEY] case 1) -> actions blocked. + ButtonStateCase(source = StatusSource.ONLY_CACHE, error = null, expectedEnabled = false), + // Expired refresh token, only cache left ([REDACTED_TASK_KEY] case 2) -> actions blocked. + ButtonStateCase( + source = StatusSource.ONLY_CACHE, + error = PaymentAccountStatusValue.Error.NotSynced, + expectedEnabled = false, + ), + // Transient error overlaid on actual data -> actions blocked. + ButtonStateCase( + source = StatusSource.ACTUAL, + error = PaymentAccountStatusValue.Error.Unavailable, + expectedEnabled = false, + ), + ) + } +} \ No newline at end of file