Updated on 2026-08-14
This commit is contained in:
parent
e70f8be7c8
commit
494c9c0127
13 changed files with 508 additions and 94 deletions
|
|
@ -10,7 +10,6 @@ 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
|
||||
|
|
@ -18,6 +17,8 @@ import com.tangem.domain.models.pay.TangemPayCardState
|
|||
import com.tangem.domain.models.pay.isFrozen
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.utils.TangemPayDetailIntents
|
||||
import com.tangem.features.tangempay.utils.hasWithdrawableAmount
|
||||
import com.tangem.features.tangempay.utils.isFresh
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
@ -60,11 +61,12 @@ internal class TangemPayDetailsStateFactory(
|
|||
}
|
||||
|
||||
fun getLoadedState(status: PaymentAccountStatusValue.Loaded): TangemPayDetailsUM {
|
||||
val isFresh = status.source == StatusSource.ACTUAL && status.error == null
|
||||
val isFresh = status.isFresh
|
||||
val hasUnfrozenCard = status.cards.any { it.frozenState == TangemPayCardFrozenState.Unfrozen }
|
||||
val hasIssuingCard = status.cards.any { it.state == TangemPayCardState.Issuing }
|
||||
val isAddCardEnabled = isFresh && !hasIssuingCard
|
||||
val areActionButtonsEnabled = isFresh && hasUnfrozenCard
|
||||
val hasWithdrawableBalance = status.balance.hasWithdrawableAmount
|
||||
return TangemPayDetailsUM(
|
||||
topBarConfig = TangemPayDetailsTopBarConfig(
|
||||
onBackClick = onBack,
|
||||
|
|
@ -77,7 +79,10 @@ internal class TangemPayDetailsStateFactory(
|
|||
onRefresh = intents::onRefreshSwipe,
|
||||
),
|
||||
balanceBlockState = TangemPayDetailsBalanceBlockState.Loading(
|
||||
actionButtons = getActionButtonsConfig(isEnabled = areActionButtonsEnabled),
|
||||
actionButtons = getActionButtonsConfig(
|
||||
isAddFundsEnabled = areActionButtonsEnabled,
|
||||
isWithdrawEnabled = areActionButtonsEnabled && hasWithdrawableBalance,
|
||||
),
|
||||
cardsBlockState = TangemPayDetailsBalanceBlockState.CardsBlockState(
|
||||
cards = status.cards
|
||||
.let { if (isMultipleCardsEnabled) it else it.take(1) }
|
||||
|
|
@ -113,7 +118,7 @@ internal class TangemPayDetailsStateFactory(
|
|||
else -> null
|
||||
}
|
||||
|
||||
fun getDeactivatedState(): TangemPayDetailsUM {
|
||||
fun getDeactivatedState(hasWithdrawableBalance: Boolean): TangemPayDetailsUM {
|
||||
return TangemPayDetailsUM(
|
||||
topBarConfig = TangemPayDetailsTopBarConfig(
|
||||
onBackClick = onBack,
|
||||
|
|
@ -126,7 +131,10 @@ internal class TangemPayDetailsStateFactory(
|
|||
onRefresh = intents::onRefreshSwipe,
|
||||
),
|
||||
balanceBlockState = TangemPayDetailsBalanceBlockState.Loading(
|
||||
actionButtons = getActionButtonsConfig(isEnabled = true),
|
||||
actionButtons = getActionButtonsConfig(
|
||||
isAddFundsEnabled = true,
|
||||
isWithdrawEnabled = hasWithdrawableBalance,
|
||||
),
|
||||
cardsBlockState = null,
|
||||
),
|
||||
isBalanceHidden = false,
|
||||
|
|
@ -256,23 +264,32 @@ internal class TangemPayDetailsStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
private fun getActionButtonsConfig(isEnabled: Boolean): ImmutableList<ActionButtonConfig> {
|
||||
fun getActionButtonsConfig(
|
||||
isAddFundsEnabled: Boolean,
|
||||
isWithdrawEnabled: Boolean,
|
||||
): ImmutableList<TangemPayActionButtonUM> {
|
||||
return persistentListOf(
|
||||
ActionButtonConfig(
|
||||
text = resourceReference(id = R.string.tangempay_card_details_add_funds),
|
||||
iconResId = if (isRedesignEnabled) {
|
||||
R.drawable.ic_arrow_down_24
|
||||
} else {
|
||||
R.drawable.ic_plus_24
|
||||
},
|
||||
onClick = intents::onClickAddFunds,
|
||||
isEnabled = isEnabled,
|
||||
TangemPayActionButtonUM(
|
||||
action = TangemPayAction.AddFunds,
|
||||
config = ActionButtonConfig(
|
||||
text = resourceReference(id = R.string.tangempay_card_details_add_funds),
|
||||
iconResId = if (isRedesignEnabled) {
|
||||
R.drawable.ic_arrow_down_24
|
||||
} else {
|
||||
R.drawable.ic_plus_24
|
||||
},
|
||||
onClick = intents::onClickAddFunds,
|
||||
isEnabled = isAddFundsEnabled,
|
||||
),
|
||||
),
|
||||
ActionButtonConfig(
|
||||
text = resourceReference(id = R.string.tangempay_card_details_withdraw),
|
||||
iconResId = R.drawable.ic_arrow_up_24,
|
||||
onClick = intents::onClickWithdraw,
|
||||
isEnabled = isEnabled,
|
||||
TangemPayActionButtonUM(
|
||||
action = TangemPayAction.Withdraw,
|
||||
config = ActionButtonConfig(
|
||||
text = resourceReference(id = R.string.tangempay_card_details_withdraw),
|
||||
iconResId = R.drawable.ic_arrow_up_24,
|
||||
onClick = intents::onClickWithdraw,
|
||||
isEnabled = isWithdrawEnabled,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,26 +68,34 @@ internal sealed interface DisplayNameState {
|
|||
}
|
||||
}
|
||||
|
||||
internal enum class TangemPayAction { AddFunds, Withdraw }
|
||||
|
||||
@Immutable
|
||||
internal data class TangemPayActionButtonUM(
|
||||
val action: TangemPayAction,
|
||||
val config: ActionButtonConfig,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
internal sealed class TangemPayDetailsBalanceBlockState {
|
||||
|
||||
abstract val actionButtons: ImmutableList<ActionButtonConfig>
|
||||
abstract val actionButtons: ImmutableList<TangemPayActionButtonUM>
|
||||
abstract val cardsBlockState: CardsBlockState?
|
||||
|
||||
data class Loading(
|
||||
override val actionButtons: ImmutableList<ActionButtonConfig>,
|
||||
override val actionButtons: ImmutableList<TangemPayActionButtonUM>,
|
||||
override val cardsBlockState: CardsBlockState?,
|
||||
) : TangemPayDetailsBalanceBlockState()
|
||||
|
||||
data class Content(
|
||||
override val actionButtons: ImmutableList<ActionButtonConfig>,
|
||||
override val actionButtons: ImmutableList<TangemPayActionButtonUM>,
|
||||
override val cardsBlockState: CardsBlockState?,
|
||||
val fiatBalance: TextReference,
|
||||
val isBalanceFlickering: Boolean,
|
||||
) : TangemPayDetailsBalanceBlockState()
|
||||
|
||||
data class Error(
|
||||
override val actionButtons: ImmutableList<ActionButtonConfig>,
|
||||
override val actionButtons: ImmutableList<TangemPayActionButtonUM>,
|
||||
override val cardsBlockState: CardsBlockState?,
|
||||
) : TangemPayDetailsBalanceBlockState()
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import com.tangem.domain.feedback.models.WalletMetaInfo
|
|||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
||||
import com.tangem.domain.pay.model.TangemPayTopUpData
|
||||
|
|
@ -113,7 +114,9 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
val uiState: StateFlow<TangemPayDetailsUM>
|
||||
field = MutableStateFlow(
|
||||
when {
|
||||
params.initialStatus.isDeactivated -> stateFactory.getDeactivatedState()
|
||||
params.initialStatus.isDeactivated -> stateFactory.getDeactivatedState(
|
||||
hasWithdrawableBalance = params.initialStatus.balanceOrNull()?.hasWithdrawableAmount == true,
|
||||
)
|
||||
else -> stateFactory.getLoadingState()
|
||||
},
|
||||
)
|
||||
|
|
@ -134,7 +137,11 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
.onEach { state ->
|
||||
when (state) {
|
||||
is PaymentAccountStatusValue.Deactivated -> {
|
||||
uiState.update { stateFactory.getDeactivatedState() }
|
||||
uiState.update {
|
||||
stateFactory.getDeactivatedState(
|
||||
hasWithdrawableBalance = state.balance.hasWithdrawableAmount,
|
||||
)
|
||||
}
|
||||
uiState.update(DetailsBalanceTransformer(state.balance.fiatBalance))
|
||||
}
|
||||
is PaymentAccountStatusValue.Loaded -> {
|
||||
|
|
@ -169,7 +176,21 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
frozenStateJobHolder.cancel()
|
||||
cardDetailsRepository
|
||||
.cardFrozenState(cardId)
|
||||
.onEach { uiState.update(TangemPayFreezeUnfreezeStateTransformer(cardFrozenState = it)) }
|
||||
.onEach { frozenState ->
|
||||
// Mirror getLoadedState gating so a live freeze update can't re-enable actions on stale data.
|
||||
val isFresh = currentStatus.value.ifLoadedOrNull { it.isFresh } == true
|
||||
val isUnfrozen = frozenState == TangemPayCardFrozenState.Unfrozen
|
||||
val areActionButtonsEnabled = isFresh && isUnfrozen
|
||||
val hasWithdrawableBalance = currentStatus.value.balanceOrNull()?.hasWithdrawableAmount == true
|
||||
uiState.update(
|
||||
TangemPayActionButtonsTransformer(
|
||||
stateFactory.getActionButtonsConfig(
|
||||
isAddFundsEnabled = areActionButtonsEnabled,
|
||||
isWithdrawEnabled = areActionButtonsEnabled && hasWithdrawableBalance,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
.saveIn(frozenStateJobHolder)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.tangempay.model.transformers
|
||||
|
||||
import com.tangem.features.tangempay.entity.TangemPayActionButtonUM
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal class TangemPayActionButtonsTransformer(
|
||||
private val actionButtons: ImmutableList<TangemPayActionButtonUM>,
|
||||
) : Transformer<TangemPayDetailsUM> {
|
||||
|
||||
override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM {
|
||||
val balanceBlockState = prevState.balanceBlockState
|
||||
if (balanceBlockState !is TangemPayDetailsBalanceBlockState.Content) return prevState
|
||||
return prevState.copy(balanceBlockState = balanceBlockState.copy(actionButtons = actionButtons))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
package com.tangem.features.tangempay.model.transformers
|
||||
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class TangemPayFreezeUnfreezeStateTransformer(
|
||||
private val cardFrozenState: TangemPayCardFrozenState,
|
||||
) : Transformer<TangemPayDetailsUM> {
|
||||
|
||||
override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM {
|
||||
val balanceBlockState = if (prevState.balanceBlockState is TangemPayDetailsBalanceBlockState.Content) {
|
||||
val actionButtons = prevState.balanceBlockState.actionButtons.map {
|
||||
it.copy(isEnabled = cardFrozenState == TangemPayCardFrozenState.Unfrozen)
|
||||
}
|
||||
prevState.balanceBlockState.copy(actionButtons = actionButtons.toPersistentList())
|
||||
} else {
|
||||
prevState.balanceBlockState
|
||||
}
|
||||
return prevState.copy(balanceBlockState = balanceBlockState)
|
||||
}
|
||||
}
|
||||
|
|
@ -61,6 +61,7 @@ import com.tangem.features.tangempay.entity.*
|
|||
import com.tangem.features.tokendetails.ExpressTransactionsComponent
|
||||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
private const val DISABLED_ALPHA = 0.5f
|
||||
|
||||
|
|
@ -283,7 +284,7 @@ private fun TangemPayDetailsBalanceBlock(
|
|||
if (state.actionButtons.isNotEmpty()) {
|
||||
HorizontalActionChips(
|
||||
modifier = Modifier.padding(top = 12.dp),
|
||||
buttons = state.actionButtons,
|
||||
buttons = state.actionButtons.map { it.config }.toImmutableList(),
|
||||
containerColor = TangemTheme.colors.background.primary,
|
||||
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
|
|
@ -468,10 +469,13 @@ internal class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<T
|
|||
pullToRefreshConfig = PullToRefreshConfig(isRefreshing = false, onRefresh = {}),
|
||||
balanceBlockState = TangemPayDetailsBalanceBlockState.Content(
|
||||
actionButtons = persistentListOf(
|
||||
ActionButtonConfig(
|
||||
text = resourceReference(id = R.string.common_receive),
|
||||
iconResId = R.drawable.ic_arrow_down_24,
|
||||
onClick = {},
|
||||
TangemPayActionButtonUM(
|
||||
action = TangemPayAction.AddFunds,
|
||||
config = ActionButtonConfig(
|
||||
text = resourceReference(id = R.string.common_receive),
|
||||
iconResId = R.drawable.ic_arrow_down_24,
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
),
|
||||
fiatBalance = combinedReference(
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.SpacerH24
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
|
||||
import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshSlidingContainer
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.components.text.applyBladeBrush
|
||||
|
|
@ -417,7 +416,7 @@ private fun CardsBlock(
|
|||
|
||||
@Composable
|
||||
private fun LazyItemScope.ActionBlock(
|
||||
actionButtons: ImmutableList<ActionButtonConfig>,
|
||||
actionButtons: ImmutableList<TangemPayActionButtonUM>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
|
|
@ -427,14 +426,15 @@ private fun LazyItemScope.ActionBlock(
|
|||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
actionButtons.fastForEach { actionConfig ->
|
||||
actionButtons.fastForEach { actionButton ->
|
||||
val config = actionButton.config
|
||||
TangemPayActionButton(
|
||||
modifier = Modifier.testTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON),
|
||||
iconRes = actionConfig.iconResId,
|
||||
onClick = actionConfig.onClick,
|
||||
isEnabled = actionConfig.isEnabled,
|
||||
isLoading = actionConfig.isInProgress,
|
||||
title = actionConfig.text,
|
||||
iconRes = config.iconResId,
|
||||
onClick = config.onClick,
|
||||
isEnabled = config.isEnabled,
|
||||
isLoading = config.isInProgress,
|
||||
title = config.text,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,12 @@ internal fun AccountStatus.Payment.balanceOrNull(): PaymentAccountStatusValue.Ba
|
|||
else -> null
|
||||
}
|
||||
|
||||
internal val PaymentAccountStatusValue.Balance.hasWithdrawableAmount: Boolean
|
||||
get() = availableForWithdrawal.signum() > 0
|
||||
|
||||
internal val PaymentAccountStatusValue.Loaded.isFresh: Boolean
|
||||
get() = source == StatusSource.ACTUAL && error == null
|
||||
|
||||
internal fun AccountStatus.Payment.findCard(
|
||||
initialCardId: String,
|
||||
initialStatus: AccountStatus.Payment,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.tangempay
|
||||
|
||||
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
|
||||
import com.tangem.features.tangempay.entity.TangemPayAction
|
||||
import com.tangem.features.tangempay.entity.TangemPayActionButtonUM
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
|
||||
|
||||
internal val List<TangemPayActionButtonUM>.withdrawButton: ActionButtonConfig
|
||||
get() = first { it.action == TangemPayAction.Withdraw }.config
|
||||
|
||||
internal val List<TangemPayActionButtonUM>.addFundsButton: ActionButtonConfig
|
||||
get() = first { it.action == TangemPayAction.AddFunds }.config
|
||||
|
||||
internal val TangemPayDetailsUM.withdrawButton: ActionButtonConfig
|
||||
get() = balanceBlockState.actionButtons.withdrawButton
|
||||
|
||||
internal val TangemPayDetailsUM.addFundsButton: ActionButtonConfig
|
||||
get() = balanceBlockState.actionButtons.addFundsButton
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.features.tangempay
|
||||
|
||||
import com.tangem.domain.models.pay.TangemPayCard
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.domain.models.pay.TangemPayCardState
|
||||
|
||||
internal fun tangemPayCard(
|
||||
id: String = "card_1",
|
||||
lastDigits: String = "1234",
|
||||
frozenState: TangemPayCardFrozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
state: TangemPayCardState = TangemPayCardState.Active,
|
||||
): TangemPayCard = TangemPayCard(
|
||||
id = id,
|
||||
productInstanceId = "product_1",
|
||||
cardStatus = TangemPayCard.Status.ACTIVE,
|
||||
hasPinCode = true,
|
||||
displayName = null,
|
||||
limit = null,
|
||||
frozenState = frozenState,
|
||||
lastDigits = lastDigits,
|
||||
state = state,
|
||||
)
|
||||
|
|
@ -6,7 +6,10 @@ 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.addFundsButton
|
||||
import com.tangem.features.tangempay.tangemPayCard
|
||||
import com.tangem.features.tangempay.utils.TangemPayDetailIntents
|
||||
import com.tangem.features.tangempay.withdrawButton
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
|
@ -14,29 +17,15 @@ 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
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class TangemPayDetailsStateFactoryTest {
|
||||
|
||||
private val intents: TangemPayDetailIntents = mockk(relaxed = true)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(intents)
|
||||
}
|
||||
private val activeUnfrozenCard = tangemPayCard()
|
||||
|
||||
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(
|
||||
private val factory = TangemPayDetailsStateFactory(
|
||||
onBack = {},
|
||||
onOpenMenu = {},
|
||||
intents = intents,
|
||||
|
|
@ -45,14 +34,9 @@ internal class TangemPayDetailsStateFactoryTest {
|
|||
isMultipleCardsEnabled = true,
|
||||
)
|
||||
|
||||
private fun loadedStatus(
|
||||
statusSource: StatusSource,
|
||||
statusError: PaymentAccountStatusValue.Error?,
|
||||
statusCards: List<TangemPayCard> = listOf(activeUnfrozenCard),
|
||||
): PaymentAccountStatusValue.Loaded = mockk(relaxed = true) {
|
||||
every { source } returns statusSource
|
||||
every { error } returns statusError
|
||||
every { cards } returns statusCards
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(intents)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
|
|
@ -61,14 +45,13 @@ internal class TangemPayDetailsStateFactoryTest {
|
|||
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 }
|
||||
val actionButtonsEnabled = state.balanceBlockState.actionButtons.map { it.config.isEnabled }
|
||||
assertThat(actionButtonsEnabled).containsExactly(case.expectedEnabled, case.expectedEnabled)
|
||||
assertThat(state.balanceBlockState.cardsBlockState?.isAddCardEnabled).isEqualTo(case.expectedEnabled)
|
||||
// The card tile is intentionally NOT source-gated: it stays clickable on stale data as long as
|
||||
|
|
@ -80,7 +63,6 @@ internal class TangemPayDetailsStateFactoryTest {
|
|||
@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,
|
||||
|
|
@ -92,14 +74,13 @@ internal class TangemPayDetailsStateFactoryTest {
|
|||
val state = factory.getLoadedState(status)
|
||||
|
||||
// Assert
|
||||
assertThat(state.balanceBlockState.actionButtons.map { it.isEnabled }).containsExactly(false, false)
|
||||
assertThat(state.balanceBlockState.actionButtons.map { it.config.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,
|
||||
|
|
@ -114,6 +95,87 @@ internal class TangemPayDetailsStateFactoryTest {
|
|||
assertThat(state.balanceBlockState.cardsBlockState?.isAddCardEnabled).isFalse()
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideBalanceCases")
|
||||
fun `GIVEN fresh status WHEN getLoadedState THEN withdraw gated by balance but add funds enabled`(
|
||||
case: BalanceCase,
|
||||
) {
|
||||
// Arrange
|
||||
val status = loadedStatus(availableForWithdrawal = case.availableForWithdrawal)
|
||||
|
||||
// Act
|
||||
val state = factory.getLoadedState(status)
|
||||
|
||||
// Assert
|
||||
assertThat(state.addFundsButton.isEnabled).isTrue()
|
||||
assertThat(state.withdrawButton.isEnabled).isEqualTo(case.expectedWithdrawEnabled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN deactivated with positive balance WHEN getDeactivatedState THEN withdraw enabled`() {
|
||||
// Act
|
||||
val state = factory.getDeactivatedState(hasWithdrawableBalance = true)
|
||||
|
||||
// Assert
|
||||
assertThat(state.addFundsButton.isEnabled).isTrue()
|
||||
assertThat(state.withdrawButton.isEnabled).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN deactivated with zero balance WHEN getDeactivatedState THEN withdraw disabled`() {
|
||||
// Act
|
||||
val state = factory.getDeactivatedState(hasWithdrawableBalance = false)
|
||||
|
||||
// Assert
|
||||
assertThat(state.addFundsButton.isEnabled).isTrue()
|
||||
assertThat(state.withdrawButton.isEnabled).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN withdraw disabled WHEN getActionButtonsConfig THEN withdraw disabled and add funds enabled`() {
|
||||
// Act
|
||||
val buttons = factory.getActionButtonsConfig(isAddFundsEnabled = true, isWithdrawEnabled = false)
|
||||
|
||||
// Assert
|
||||
assertThat(buttons.addFundsButton.isEnabled).isTrue()
|
||||
assertThat(buttons.withdrawButton.isEnabled).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN both enabled WHEN getActionButtonsConfig THEN both buttons enabled`() {
|
||||
// Act
|
||||
val buttons = factory.getActionButtonsConfig(isAddFundsEnabled = true, isWithdrawEnabled = true)
|
||||
|
||||
// Assert
|
||||
assertThat(buttons.addFundsButton.isEnabled).isTrue()
|
||||
assertThat(buttons.withdrawButton.isEnabled).isTrue()
|
||||
}
|
||||
|
||||
private fun loadedStatus(
|
||||
statusSource: StatusSource = StatusSource.ACTUAL,
|
||||
statusError: PaymentAccountStatusValue.Error? = null,
|
||||
statusCards: List<TangemPayCard> = listOf(activeUnfrozenCard),
|
||||
availableForWithdrawal: BigDecimal = BigDecimal.TEN,
|
||||
): PaymentAccountStatusValue.Loaded = mockk(relaxed = true) {
|
||||
every { source } returns statusSource
|
||||
every { error } returns statusError
|
||||
every { cards } returns statusCards
|
||||
every { balance } returns PaymentAccountStatusValue.Balance(
|
||||
fiatBalance = PaymentAccountStatusValue.FiatBalance(
|
||||
availableBalance = BigDecimal.ZERO,
|
||||
currency = "USD",
|
||||
),
|
||||
cryptoBalance = PaymentAccountStatusValue.CryptoBalance(
|
||||
id = "id",
|
||||
chainId = 1L,
|
||||
depositAddress = "address",
|
||||
tokenContractAddress = "contract",
|
||||
balance = BigDecimal.ZERO,
|
||||
),
|
||||
availableForWithdrawal = availableForWithdrawal,
|
||||
)
|
||||
}
|
||||
|
||||
internal data class ButtonStateCase(
|
||||
val source: StatusSource,
|
||||
val error: PaymentAccountStatusValue.Error?,
|
||||
|
|
@ -121,7 +183,19 @@ internal class TangemPayDetailsStateFactoryTest {
|
|||
val expectedCardEnabled: Boolean,
|
||||
)
|
||||
|
||||
internal data class BalanceCase(
|
||||
val availableForWithdrawal: BigDecimal,
|
||||
val expectedWithdrawEnabled: Boolean,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
@JvmStatic
|
||||
fun provideBalanceCases() = listOf(
|
||||
BalanceCase(availableForWithdrawal = BigDecimal.ZERO, expectedWithdrawEnabled = false),
|
||||
BalanceCase(availableForWithdrawal = BigDecimal.TEN, expectedWithdrawEnabled = true),
|
||||
BalanceCase(availableForWithdrawal = BigDecimal("-1"), expectedWithdrawEnabled = false),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun provideButtonStateCases() = listOf(
|
||||
// Fresh data from the network -> actions allowed, card tile clickable.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,171 @@
|
|||
package com.tangem.features.tangempay.model
|
||||
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
import com.tangem.features.tangempay.addFundsButton
|
||||
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
|
||||
import com.tangem.features.tangempay.tangemPayCard
|
||||
import com.tangem.features.tangempay.withdrawButton
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
import java.math.BigDecimal
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class TangemPayDetailsModelTest {
|
||||
|
||||
private val userWalletId = UserWalletId("123")
|
||||
|
||||
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier = mockk()
|
||||
private val cardDetailsRepository: TangemPayCardDetailsRepository = mockk(relaxed = true)
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideFreezeCases")
|
||||
fun `GIVEN frozen state and balance WHEN status loaded THEN action buttons gated accordingly`(
|
||||
case: FreezeCase,
|
||||
) = runTest {
|
||||
// Arrange + Act
|
||||
val model = createModel(
|
||||
testScope = this,
|
||||
statusSource = case.statusSource,
|
||||
frozenState = case.frozenState,
|
||||
availableForWithdrawal = case.availableForWithdrawal,
|
||||
)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
val state = model.uiState.value
|
||||
assertThat(state.addFundsButton.isEnabled).isEqualTo(case.expectedAddFundsEnabled)
|
||||
assertThat(state.withdrawButton.isEnabled).isEqualTo(case.expectedWithdrawEnabled)
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
private fun createModel(
|
||||
testScope: TestScope,
|
||||
statusSource: StatusSource,
|
||||
frozenState: TangemPayCardFrozenState,
|
||||
availableForWithdrawal: BigDecimal,
|
||||
): TangemPayDetailsModel {
|
||||
val loaded: PaymentAccountStatusValue.Loaded = mockk(relaxed = true) {
|
||||
every { source } returns statusSource
|
||||
every { error } returns null
|
||||
every { cards } returns listOf(tangemPayCard())
|
||||
every { balance } returns PaymentAccountStatusValue.Balance(
|
||||
fiatBalance = PaymentAccountStatusValue.FiatBalance(
|
||||
availableBalance = BigDecimal.ZERO,
|
||||
currency = "USD",
|
||||
),
|
||||
cryptoBalance = PaymentAccountStatusValue.CryptoBalance(
|
||||
id = "id",
|
||||
chainId = 1L,
|
||||
depositAddress = "address",
|
||||
tokenContractAddress = "contract",
|
||||
balance = BigDecimal.ZERO,
|
||||
),
|
||||
availableForWithdrawal = availableForWithdrawal,
|
||||
)
|
||||
}
|
||||
val paymentStatus: AccountStatus.Payment = mockk(relaxed = true) {
|
||||
every { value } returns loaded
|
||||
every { account } returns mockk(relaxed = true) {
|
||||
every { userWalletId } returns this@TangemPayDetailsModelTest.userWalletId
|
||||
}
|
||||
}
|
||||
val params = TangemPayDetailsContainerComponent.Params(initialStatus = paymentStatus)
|
||||
|
||||
every { paymentAccountStatusSupplier.invoke(any<UserWalletId>()) } returns flowOf(paymentStatus)
|
||||
every { cardDetailsRepository.cardFrozenState(any()) } returns flowOf(frozenState)
|
||||
coEvery { cardDetailsRepository.isAddToWalletDone(any()) } returns false.right()
|
||||
|
||||
return TangemPayDetailsModel(
|
||||
paramsContainer = MutableParamsContainer(params),
|
||||
paymentAccountStatusSupplier = paymentAccountStatusSupplier,
|
||||
dispatchers = testScope.createTestingCoroutineDispatcherProvider(),
|
||||
analytics = mockk(relaxed = true),
|
||||
router = mockk(relaxed = true),
|
||||
urlOpener = mockk(relaxed = true),
|
||||
cardDetailsRepository = cardDetailsRepository,
|
||||
getBalanceHidingSettingsUseCase = mockk(relaxed = true),
|
||||
uiMessageSender = mockk(relaxed = true),
|
||||
txHistoryUpdateListener = mockk(relaxed = true),
|
||||
tangemPayWithdrawRepository = mockk(relaxed = true),
|
||||
sendFeedbackEmailUseCase = mockk(relaxed = true),
|
||||
expressTransactionsEventListener = mockk(relaxed = true),
|
||||
tangemPayFeatureToggles = mockk(relaxed = true),
|
||||
paymentAccountStatusFetcher = mockk(relaxed = true),
|
||||
produceTangemPayInitialDataUseCase = mockk(relaxed = true),
|
||||
onboardingRepository = mockk(relaxed = true),
|
||||
getCustomerOffers = mockk(relaxed = true),
|
||||
)
|
||||
}
|
||||
|
||||
private fun TestScope.createTestingCoroutineDispatcherProvider(): TestingCoroutineDispatcherProvider {
|
||||
val testDispatcher = StandardTestDispatcher(testScheduler)
|
||||
return TestingCoroutineDispatcherProvider(
|
||||
main = testDispatcher,
|
||||
mainImmediate = testDispatcher,
|
||||
io = testDispatcher,
|
||||
default = testDispatcher,
|
||||
single = testDispatcher,
|
||||
)
|
||||
}
|
||||
|
||||
internal data class FreezeCase(
|
||||
val statusSource: StatusSource,
|
||||
val frozenState: TangemPayCardFrozenState,
|
||||
val availableForWithdrawal: BigDecimal,
|
||||
val expectedAddFundsEnabled: Boolean,
|
||||
val expectedWithdrawEnabled: Boolean,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
@JvmStatic
|
||||
fun provideFreezeCases() = listOf(
|
||||
FreezeCase(
|
||||
statusSource = StatusSource.ACTUAL,
|
||||
frozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
availableForWithdrawal = BigDecimal.ZERO,
|
||||
expectedAddFundsEnabled = true,
|
||||
expectedWithdrawEnabled = false,
|
||||
),
|
||||
FreezeCase(
|
||||
statusSource = StatusSource.ACTUAL,
|
||||
frozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
availableForWithdrawal = BigDecimal.TEN,
|
||||
expectedAddFundsEnabled = true,
|
||||
expectedWithdrawEnabled = true,
|
||||
),
|
||||
FreezeCase(
|
||||
statusSource = StatusSource.ACTUAL,
|
||||
frozenState = TangemPayCardFrozenState.Frozen,
|
||||
availableForWithdrawal = BigDecimal.TEN,
|
||||
expectedAddFundsEnabled = false,
|
||||
expectedWithdrawEnabled = false,
|
||||
),
|
||||
FreezeCase(
|
||||
statusSource = StatusSource.CACHE,
|
||||
frozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
availableForWithdrawal = BigDecimal.TEN,
|
||||
expectedAddFundsEnabled = false,
|
||||
expectedWithdrawEnabled = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.tangem.features.tangempay.model.transformers
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
|
||||
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.TangemPayAction
|
||||
import com.tangem.features.tangempay.entity.TangemPayActionButtonUM
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsTopBarConfig
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class TangemPayActionButtonsTransformerTest {
|
||||
|
||||
@Test
|
||||
fun `GIVEN content balance block WHEN transform THEN action buttons are replaced`() {
|
||||
// Arrange
|
||||
val newButtons = persistentListOf(actionButton(enabled = false))
|
||||
val transformer = TangemPayActionButtonsTransformer(newButtons)
|
||||
|
||||
// Act
|
||||
val result = transformer.transform(contentState())
|
||||
|
||||
// Assert
|
||||
assertThat(result.balanceBlockState.actionButtons).isEqualTo(newButtons)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-content balance block WHEN transform THEN state is unchanged`() {
|
||||
// Arrange
|
||||
val transformer = TangemPayActionButtonsTransformer(persistentListOf(actionButton()))
|
||||
val state = contentState().copy(
|
||||
balanceBlockState = TangemPayDetailsBalanceBlockState.Loading(
|
||||
actionButtons = persistentListOf(),
|
||||
cardsBlockState = null,
|
||||
),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = transformer.transform(state)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(state)
|
||||
}
|
||||
|
||||
private fun contentState(): TangemPayDetailsUM = TangemPayDetailsUM(
|
||||
topBarConfig = TangemPayDetailsTopBarConfig(
|
||||
onBackClick = {},
|
||||
onOpenMenu = {},
|
||||
items = persistentListOf(),
|
||||
itemsV2 = persistentListOf(),
|
||||
),
|
||||
pullToRefreshConfig = PullToRefreshConfig(isRefreshing = false, onRefresh = {}),
|
||||
balanceBlockState = TangemPayDetailsBalanceBlockState.Content(
|
||||
actionButtons = persistentListOf(actionButton()),
|
||||
cardsBlockState = null,
|
||||
fiatBalance = TextReference.EMPTY,
|
||||
isBalanceFlickering = false,
|
||||
),
|
||||
addToWalletBlockState = null,
|
||||
isBalanceHidden = false,
|
||||
errorNotificationConfig = null,
|
||||
accountDeactivatedNotificationConfig = null,
|
||||
)
|
||||
|
||||
private fun actionButton(enabled: Boolean = true): TangemPayActionButtonUM = TangemPayActionButtonUM(
|
||||
action = TangemPayAction.Withdraw,
|
||||
config = ActionButtonConfig(
|
||||
text = resourceReference(R.string.tangempay_card_details_withdraw),
|
||||
iconResId = R.drawable.ic_arrow_up_24,
|
||||
onClick = {},
|
||||
isEnabled = enabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue