diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 4446e6ab40..a6add9266b 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -93,7 +93,7 @@ }, { "name": "TWI_1377_MANAGE_FUNDS", - "version": "5.40" + "version": "6.0" }, { "name": "AND_15009_SWAP_PROVIDER_FILTER_ENABLED", diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt index 52caca9648..c320bb0d22 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt @@ -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 diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt index 302845e5ed..9ffbdbb1ce 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt @@ -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), ) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt index 46530d78b1..1a67d0f983 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt @@ -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) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemHoldToConfirmButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemHoldToConfirmButton.kt index 9e5c8386c2..cf2e51371b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemHoldToConfirmButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemHoldToConfirmButton.kt @@ -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, diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt index 88202ae1fe..75495637aa 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt @@ -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) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt index 244162936d..bd91adc9f8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt @@ -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. diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt index 6ef457f21c..45e60128ee 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt @@ -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), ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds2/topnavigation/TangemTopNavigation.kt b/core/ui/src/main/java/com/tangem/core/ui/ds2/topnavigation/TangemTopNavigation.kt index 14c26f0864..9473717202 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds2/topnavigation/TangemTopNavigation.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds2/topnavigation/TangemTopNavigation.kt @@ -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 -> diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt index b82ddab432..1000954081 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt @@ -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() diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt index 6922f8a91b..22fb987794 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt @@ -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( diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/StoriesButton.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/StoriesButton.kt index ddcc652d3f..23bd2f574b 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/StoriesButton.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/StoriesButton.kt @@ -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 diff --git a/features/promo-banners/api/src/main/kotlin/com/tangem/features/promobanners/api/PromoBannersBlockComponent.kt b/features/promo-banners/api/src/main/kotlin/com/tangem/features/promobanners/api/PromoBannersBlockComponent.kt index 57c0e3844a..d2eb78c1ae 100644 --- a/features/promo-banners/api/src/main/kotlin/com/tangem/features/promobanners/api/PromoBannersBlockComponent.kt +++ b/features/promo-banners/api/src/main/kotlin/com/tangem/features/promobanners/api/PromoBannersBlockComponent.kt @@ -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) diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/DefaultPromoBannersBlockComponent.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/DefaultPromoBannersBlockComponent.kt index 814031d197..fc2b86a4e4 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/DefaultPromoBannersBlockComponent.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/DefaultPromoBannersBlockComponent.kt @@ -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, ) } diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/ui/PromoBannersBlock.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/ui/PromoBannersBlock.kt index c6a4666d66..1d13b403b0 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/ui/PromoBannersBlock.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/ui/PromoBannersBlock.kt @@ -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, 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 \ No newline at end of file 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..c13bd904e7 --- /dev/null +++ b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactoryTest.kt @@ -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 = 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, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt index 87135b6a10..f1a93abe4b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt @@ -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 -> diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt index f59bc324b5..844acea245 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt @@ -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)) } } diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt index 8f922d63a6..e0c1ec5b25 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt @@ -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\$")) }