Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-24 22:35:09 +04:00
commit ec06f2e51c
20 changed files with 260 additions and 101 deletions

View file

@ -93,7 +93,7 @@
},
{
"name": "TWI_1377_MANAGE_FUNDS",
"version": "5.40"
"version": "6.0"
},
{
"name": "AND_15009_SWAP_PROVIDER_FILTER_ENABLED",

View file

@ -10,7 +10,6 @@ import androidx.compose.material3.ButtonColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
@ -190,7 +189,6 @@ fun PrimaryButtonIconStart(
enabled: Boolean = true,
tint: Color? = null,
size: TangemButtonSize = TangemButtonSize.Default,
shape: Shape = size.toShape(),
) {
TangemButton(
modifier = modifier,
@ -205,7 +203,6 @@ fun PrimaryButtonIconStart(
showProgress = showProgress,
textStyle = TangemTheme.typography.subtitle1,
size = size,
shape = shape,
)
}
// endregion PrimaryButton
@ -219,7 +216,6 @@ fun SecondaryButton(
showProgress: Boolean = false,
enabled: Boolean = true,
size: TangemButtonSize = TangemButtonSize.Default,
shape: Shape = size.toShape(),
) {
TangemButton(
modifier = modifier,
@ -230,7 +226,6 @@ fun SecondaryButton(
enabled = enabled,
showProgress = showProgress,
size = size,
shape = shape,
textStyle = TangemTheme.typography.subtitle1,
)
}
@ -248,7 +243,6 @@ fun SecondaryButtonIconEnd(
enabled: Boolean = true,
tint: Color? = null,
size: TangemButtonSize = TangemButtonSize.Default,
shape: Shape = size.toShape(),
) {
TangemButton(
modifier = modifier,
@ -263,7 +257,6 @@ fun SecondaryButtonIconEnd(
showProgress = showProgress,
textStyle = TangemTheme.typography.subtitle1,
size = size,
shape = shape,
)
}
@ -280,7 +273,6 @@ fun SecondaryButtonIconStart(
enabled: Boolean = true,
iconTint: Color? = null,
size: TangemButtonSize = TangemButtonSize.Default,
shape: Shape = size.toShape(),
) {
TangemButton(
modifier = modifier,
@ -295,7 +287,6 @@ fun SecondaryButtonIconStart(
showProgress = showProgress,
textStyle = TangemTheme.typography.subtitle1,
size = size,
shape = shape,
)
}
// endregion SecondaryButton

View file

@ -6,6 +6,7 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
@ -14,7 +15,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
@ -42,7 +42,6 @@ fun TangemButton(
size: TangemButtonSize = TangemButtonSize.Default,
elevation: ButtonElevation = TangemButtonsDefaults.elevation,
textStyle: TextStyle = TangemTheme.typography.button,
shape: Shape = size.toShape(),
iconPadding: Dp = size.toIconPadding(),
animateContentChange: Boolean = false,
) {
@ -57,7 +56,7 @@ fun TangemButton(
},
enabled = enabled,
elevation = elevation,
shape = shape,
shape = CircleShape,
colors = colors,
contentPadding = size.toContentPadding(icon = icon),
) {

View file

@ -3,7 +3,6 @@ package com.tangem.core.ui.components.buttons.common
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
@ -32,19 +31,6 @@ internal fun TangemButtonSize.toHeightDp(): Dp = when (this) {
TangemButtonSize.Small -> 24.dp
}
@Composable
@ReadOnlyComposable
internal fun TangemButtonSize.toShape(): Shape = when (this) {
TangemButtonSize.Default -> TangemTheme.shapes.roundedCornersXMedium
TangemButtonSize.WideAction -> TangemTheme.shapes.roundedCornersMedium
TangemButtonSize.Text -> TangemTheme.shapes.roundedCornersSmall
TangemButtonSize.Selector -> TangemTheme.shapes.roundedCornersSmall
TangemButtonSize.Action -> TangemTheme.shapes.roundedCornersMedium
TangemButtonSize.TwoLines -> TangemTheme.shapes.roundedCornersXMedium
TangemButtonSize.RoundedAction -> TangemTheme.shapes.roundedCornersLarge
TangemButtonSize.Small -> TangemTheme.shapes.roundedCornersXMedium
}
@Composable
@ReadOnlyComposable
internal fun TangemButtonSize.toIconPadding(): Dp = when (this) {

View file

@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Surface
@ -43,7 +44,6 @@ import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
@ -104,7 +104,6 @@ internal fun TangemHoldToConfirmButton(
colors: ButtonColors = TangemButtonsDefaults.primaryButtonColors,
size: TangemButtonSize = TangemButtonSize.Default,
textStyle: TextStyle = TangemTheme.typography.button,
shape: Shape = size.toShape(),
) {
val coroutineScope = rememberCoroutineScope()
val state = rememberHoldToConfirmState()
@ -162,14 +161,14 @@ internal fun TangemHoldToConfirmButton(
onConfirm = onConfirm,
),
),
shape = shape,
shape = CircleShape,
color = containerColor,
) {
HoldToConfirmButtonContent(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = buttonHeight)
.clip(shape),
.clip(CircleShape),
contentPadding = contentPadding,
state = state,
shouldShowProgress = shouldShowProgress,

View file

@ -6,7 +6,7 @@ import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material.ripple.RippleAlpha
import androidx.compose.material3.CircularProgressIndicator
@ -229,19 +229,14 @@ private fun TangemButtonIcon(tangemIconUM: TangemIconUM?, isVisible: Boolean, si
/**
* Defines the shape of the Tangem button.
*/
@Deprecated("All buttons are rounded by design")
enum class TangemButtonShape {
Default,
Rounded,
;
@ReadOnlyComposable
@Composable
internal fun toShape(size: TangemButtonSize) = RoundedCornerShape(
when (this) {
Default -> size.toShapeRadius()
Rounded -> 100.dp
},
)
@Suppress("UnusedParameter")
internal fun toShape(size: TangemButtonSize) = CircleShape
}
/**
@ -309,18 +304,6 @@ enum class TangemButtonSize {
-> TangemTheme.dimens2.x7
}
@ReadOnlyComposable
@Composable
internal fun toShapeRadius() = when (this) {
X7,
X8,
X9,
X10,
-> TangemTheme.dimens2.x2
X12 -> TangemTheme.dimens2.x3
X15 -> TangemTheme.dimens2.x4
}
@ReadOnlyComposable
@Composable
internal fun toTextStyle(): TextStyle = when (this) {

View file

@ -14,7 +14,7 @@ import com.tangem.core.ui.extensions.TextReference
* @param isEnabled Boolean indicating whether the button is enabled.
* @param isLoading Boolean indicating whether the button is in a loading state.
* @param size TangemButtonSize defining the size of the button.
* @param shape TangemButtonShape defining the shape of the button.
* @param shape TangemButtonShape defining the shape of the button. UPD all buttons are rounded.
* @param type TangemButtonType defining the style type of the button.
* @param onClick Lambda to be invoked when the button is clicked.
* @param onLongClick Lambda to be invoked when the button is long-clicked.

View file

@ -18,6 +18,7 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
@ -256,7 +257,7 @@ private fun TangemMessageContent(
}
// The close button is drawn over the top-end corner, so in the leading-content layout the texts
// reserve trailing space to never run under it; trailing-content layouts are kept untouched
val isCloseSpaceReserved = hasCloseButton && leadingContent != null && content == null
val isCloseSpaceReserved = hasCloseButton && content == null
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
modifier = Modifier.padding(TangemTheme.dimens2.x1),
@ -280,7 +281,8 @@ private fun TangemMessageContent(
text = title.resolveAnnotatedReference(),
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
textAlign = textAlign,
modifier = Modifier.testTag(NotificationTestTags.TITLE),
)

View file

@ -96,16 +96,15 @@ fun TangemTopNavigation(
end = 16.dp,
),
content = {
// Each optional slot caches its last non-null content so the spring exit transition
// still has something to render after the caller flips it back to `null`.
val displayedStart = rememberLastNonNull(startButton)
AnimatedVisibility(
modifier = Modifier.layoutId(SlotId.Start),
visible = startButton != null,
enter = slotEnter,
exit = slotExit,
) {
displayedStart?.invoke()
Box(modifier = Modifier.layoutId(SlotId.Start)) {
AnimatedVisibility(
visible = startButton != null,
enter = slotEnter,
exit = slotExit,
) {
displayedStart?.invoke()
}
}
Column(
@ -121,30 +120,32 @@ fun TangemTopNavigation(
}
val displayedGroup = rememberLastNonNull(endButtonsGroup)
AnimatedVisibility(
modifier = Modifier.layoutId(SlotId.Group),
visible = endButtonsGroup != null,
enter = slotEnter,
exit = slotExit,
) {
displayedGroup?.let { group ->
TangemSurface(isMaterial = true, shape = CircleShape) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
content = group,
)
Box(modifier = Modifier.layoutId(SlotId.Group)) {
AnimatedVisibility(
visible = endButtonsGroup != null,
enter = slotEnter,
exit = slotExit,
) {
displayedGroup?.let { group ->
TangemSurface(isMaterial = true, shape = CircleShape) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
content = group,
)
}
}
}
}
val displayedEnd = rememberLastNonNull(endButton)
AnimatedVisibility(
modifier = Modifier.layoutId(SlotId.End),
visible = endButton != null,
enter = slotEnter,
exit = slotExit,
) {
displayedEnd?.invoke()
Box(modifier = Modifier.layoutId(SlotId.End)) {
AnimatedVisibility(
visible = endButton != null,
enter = slotEnter,
exit = slotExit,
) {
displayedEnd?.invoke()
}
}
},
) { measurables, constraints ->

View file

@ -6,6 +6,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
@ -17,6 +18,7 @@ import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.decompose.EmptyComposableBottomSheetComponent
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
@ -84,7 +86,9 @@ internal class DefaultFeedComponent(
FeedList(
modifier = modifier,
state = state,
promoBannersBlockComponent = promoBannersBlockComponent,
promoBannersBlockComponent = ComposableContentComponent { promoModifier ->
promoBannersBlockComponent.ContentWithPadding(modifier = promoModifier, horizontalItemPadding = 16.dp)
},
contentPadding = contentPadding,
)
bottomSheet.child?.instance?.BottomSheet()

View file

@ -114,9 +114,8 @@ private fun FeedListContent(
modifier = Modifier.padding(horizontal = 16.dp),
)
promoBannersBlockComponent?.Content(
modifier = Modifier.padding(top = 12.dp, start = 16.dp, end = 16.dp),
)
promoBannersBlockComponent?.Content(modifier = Modifier.padding(top = 12.dp))
SpacerH(32.dp)
NewsBlock(

View file

@ -24,7 +24,6 @@ internal fun StoriesButton(
colors = if (useDarkerColors) DarkerButtonColors else LighterButtonColors,
showProgress = showProgress,
enabled = true,
shape = TangemTheme.shapes.roundedCornersXMedium,
textStyle = TangemTheme.typography.subtitle1,
iconPadding = when (icon) {
is TangemButtonIconPosition.Start -> TangemTheme.dimens.spacing4

View file

@ -1,9 +1,14 @@
package com.tangem.features.promobanners.api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface PromoBannersBlockComponent : ComposableContentComponent {
interface PromoBannersBlockComponent {
@Composable
fun ContentWithPadding(horizontalItemPadding: Dp, modifier: Modifier)
fun setVisibleOnScreen(isVisible: Boolean)

View file

@ -3,6 +3,7 @@ package com.tangem.features.promobanners.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
@ -25,11 +26,12 @@ internal class DefaultPromoBannersBlockComponent @AssistedInject constructor(
}
@Composable
override fun Content(modifier: Modifier) {
override fun ContentWithPadding(horizontalItemPadding: Dp, modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
PromoBannersBlock(
state = state,
modifier = modifier,
horizontalItemPadding = horizontalItemPadding,
)
}

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import com.tangem.core.ui.components.SpacerH8
@ -36,7 +37,7 @@ import kotlin.math.ceil
import kotlin.math.floor
@Composable
internal fun PromoBannersBlock(state: PromoBannersBlockUM, modifier: Modifier = Modifier) {
internal fun PromoBannersBlock(state: PromoBannersBlockUM, horizontalItemPadding: Dp, modifier: Modifier = Modifier) {
if (state.banners.isEmpty()) return
val containerColor = bannerContainerColor(state.placeholder)
@ -51,13 +52,14 @@ internal fun PromoBannersBlock(state: PromoBannersBlockUM, modifier: Modifier =
SingleBanner(
banner = banner,
containerColor = containerColor,
modifier = modifier,
modifier = modifier.padding(horizontal = horizontalItemPadding),
)
} else {
key(state.userWalletId) {
BannersCarousel(
state = state,
containerColor = containerColor,
horizontalPadding = horizontalItemPadding,
modifier = modifier,
)
}
@ -109,7 +111,12 @@ private fun SingleBanner(banner: PromoBannerNotificationUM, containerColor: Colo
}
@Composable
private fun BannersCarousel(state: PromoBannersBlockUM, containerColor: Color, modifier: Modifier = Modifier) {
private fun BannersCarousel(
state: PromoBannersBlockUM,
containerColor: Color,
horizontalPadding: Dp,
modifier: Modifier = Modifier,
) {
val pagerState = rememberPagerState(
initialPage = state.initialPage,
pageCount = { state.banners.size },
@ -139,6 +146,7 @@ private fun BannersCarousel(state: PromoBannersBlockUM, containerColor: Color, m
banners = state.banners,
pagerState = pagerState,
containerColor = containerColor,
horizontalPadding = horizontalPadding,
modifier = Modifier.fillMaxWidth(),
)
@ -163,6 +171,7 @@ private fun SmoothHeightPager(
banners: List<PromoBannerNotificationUM>,
pagerState: PagerState,
containerColor: Color,
horizontalPadding: Dp,
modifier: Modifier = Modifier,
) {
SubcomposeLayout(modifier = modifier) { constraints ->
@ -206,6 +215,7 @@ private fun SmoothHeightPager(
) { page ->
val banner = banners.getOrNull(page) ?: return@HorizontalPager
BannerNotification(
modifier = Modifier.padding(horizontal = horizontalPadding),
config = banner.config,
containerColor = containerColor,
)
@ -253,7 +263,11 @@ private fun previewState(bannerCount: Int) = PromoBannersBlockUM(
@Composable
private fun Preview_PromoBannersBlock_Legacy() {
TangemThemePreview {
PromoBannersBlock(state = previewState(bannerCount = 2), modifier = Modifier.padding(16.dp))
PromoBannersBlock(
state = previewState(bannerCount = 2),
horizontalItemPadding = 12.dp,
modifier = Modifier.padding(vertical = 16.dp),
)
}
}
@ -262,7 +276,11 @@ private fun Preview_PromoBannersBlock_Legacy() {
@Composable
private fun Preview_PromoBannersBlock_Redesign() {
TangemThemePreviewRedesign {
PromoBannersBlock(state = previewState(bannerCount = 2), modifier = Modifier.padding(16.dp))
PromoBannersBlock(
state = previewState(bannerCount = 2),
modifier = Modifier.padding(vertical = 16.dp),
horizontalItemPadding = 12.dp,
)
}
}
// endregion

View file

@ -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) }

View file

@ -0,0 +1,164 @@
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<TangemPayCard> = 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 action buttons gated by freshness but card tile only by 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)
// The card tile is intentionally NOT source-gated: it stays clickable on stale data as long as
// there is no error, so the user can still view the card details offline (which self-gate actions).
assertThat(state.balanceBlockState.cardsBlockState?.cards?.single()?.isEnabled)
.isEqualTo(case.expectedCardEnabled)
}
@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,
val expectedCardEnabled: Boolean,
)
private companion object {
@JvmStatic
fun provideButtonStateCases() = listOf(
// Fresh data from the network -> actions allowed, card tile clickable.
ButtonStateCase(
source = StatusSource.ACTUAL,
error = null,
expectedEnabled = true,
expectedCardEnabled = true,
),
// Cache restored from disk before a refresh confirms it -> actions blocked, card tile clickable.
ButtonStateCase(
source = StatusSource.CACHE,
error = null,
expectedEnabled = false,
expectedCardEnabled = true,
),
// Internet unavailable, only cache left ([REDACTED_TASK_KEY] case 1) -> actions blocked, card tile clickable.
ButtonStateCase(
source = StatusSource.ONLY_CACHE,
error = null,
expectedEnabled = false,
expectedCardEnabled = true,
),
// Expired refresh token, only cache left ([REDACTED_TASK_KEY] case 2) -> everything blocked by the error.
ButtonStateCase(
source = StatusSource.ONLY_CACHE,
error = PaymentAccountStatusValue.Error.NotSynced,
expectedEnabled = false,
expectedCardEnabled = false,
),
// Transient error overlaid on actual data -> everything blocked by the error.
ButtonStateCase(
source = StatusSource.ACTUAL,
error = PaymentAccountStatusValue.Error.Unavailable,
expectedEnabled = false,
expectedCardEnabled = false,
),
)
}
}

View file

@ -295,11 +295,14 @@ internal class WalletComponent @AssistedInject constructor(
var headerSize by remember { mutableStateOf(0.dp) }
val dialog by dialog.subscribeAsState()
val uiState by model.uiState.collectAsStateWithLifecycle()
val promoBannersBlockComponentContentComponent = ComposableContentComponent { promoModifier ->
promoBannersBlockComponent.ContentWithPadding(modifier = promoModifier, horizontalItemPadding = 12.dp)
}
if (designFeatureToggles.isRedesignEnabled) {
WalletScreen2(
state = uiState,
promoBannersBlockComponent = promoBannersBlockComponent,
promoBannersBlockComponent = promoBannersBlockComponentContentComponent,
tangemPayComponent = tangemPayMainBlockComponent,
virtualAccountComponent = virtualAccountMainBlockComponent,
bottomSheetContent = { onExpandSheet ->
@ -316,7 +319,7 @@ internal class WalletComponent @AssistedInject constructor(
} else {
WalletScreen(
state = uiState,
promoBannersBlockComponent = promoBannersBlockComponent,
promoBannersBlockComponent = promoBannersBlockComponentContentComponent,
tangemPayComponent = tangemPayMainBlockComponent,
virtualAccountComponent = virtualAccountMainBlockComponent,
bottomSheetContent = { onExpandSheet ->

View file

@ -67,7 +67,7 @@ internal fun WalletListContent(
promoBannersBlockComponent?.let { component ->
item(key = "PromoBannersBlock") {
component.Content(modifier = itemModifier)
component.Content(modifier = Modifier.padding(top = TangemTheme.dimens2.x3))
}
}

View file

@ -29,6 +29,7 @@ internal fun BaseExtension.configureCompose(project: Project) {
contains(Regex(pattern = ":features:feed:api\$")) || // provides Composable function
contains(Regex(pattern = ":features:manage-tokens:api\$")) || // provides Composable function
contains(Regex(pattern = ":features:txhistory:api\$")) || // provides Composable function
contains(Regex(pattern = ":features:promo-banners:api\$")) || // provides Composable function
contains(Regex(pattern = ":impl\$"))
}