diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsHandler.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsHandler.kt index 3b59bc5471..02e289b3f1 100644 --- a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsHandler.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsHandler.kt @@ -105,8 +105,7 @@ class TokenActionsHandler @AssistedInject constructor( private fun isTopUpBlockedByBackupError(action: TokenActionsBSContentUM.Action, userWallet: UserWallet): Boolean { val isBlockedAction = action == TokenActionsBSContentUM.Action.Buy || - action == TokenActionsBSContentUM.Action.Receive || - action == TokenActionsBSContentUM.Action.Exchange + action == TokenActionsBSContentUM.Action.Receive if (!isBlockedAction) return false if (!isWalletBackupProblematicUseCase(userWallet)) return false 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 bc140e9bf2..dec9126e3d 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 @@ -89,7 +89,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/components/snackbar/TangemTopSnackbar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemTopSnackbar.kt index ac9d9a4f19..e31a9341d0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemTopSnackbar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemTopSnackbar.kt @@ -11,9 +11,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerW @@ -37,6 +39,10 @@ import com.tangem.core.ui.res.TangemThemePreviewRedesign @Suppress("LongMethod") @Composable fun TangemTopSnackbar(snackbarMessage: SnackbarMessage, modifier: Modifier = Modifier) { + val density = LocalDensity.current + val fixedFontScaleDensity = remember(density) { + Density(density = density.density, fontScale = 1f) + } val actionLabel = snackbarMessage.actionLabel val action = snackbarMessage.action val hasAction = actionLabel != null && action != null @@ -46,12 +52,7 @@ fun TangemTopSnackbar(snackbarMessage: SnackbarMessage, modifier: Modifier = Mod snackbarMessage.startIconId, hasAction, ) { mutableStateOf(false) } - val shape = if (isTextOverflowing) { - RoundedCornerShape(TangemTheme.dimens2.x5) - } else { - TangemTheme.shapes.roundedCornersXLarge - } - + val shape = RoundedCornerShape(24.dp) Column( modifier = modifier .shadow(elevation = TangemTheme.dimens.elevation4, shape = shape, clip = false) @@ -59,8 +60,8 @@ fun TangemTopSnackbar(snackbarMessage: SnackbarMessage, modifier: Modifier = Mod .clip(shape) .hazeEffectTangem() .sizeIn(minHeight = TangemTheme.dimens2.x11) - .padding(start = TangemTheme.dimens2.x5, end = TangemTheme.dimens2.x3) - .padding(vertical = TangemTheme.dimens2.x1), + .padding(start = TangemTheme.dimens2.x4, end = TangemTheme.dimens2.x3) + .padding(vertical = TangemTheme.dimens2.x2), verticalArrangement = Arrangement.Center, ) { Row( @@ -78,18 +79,20 @@ fun TangemTopSnackbar(snackbarMessage: SnackbarMessage, modifier: Modifier = Mod SpacerW(TangemTheme.dimens2.x2) } - Text( - text = snackbarMessage.message.resolveReference(), - modifier = Modifier.weight(1f, fill = false), - color = TangemTheme.colors.text.secondary, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - style = TangemTheme.typography.body2, - onTextLayout = { layoutResult -> - val isOverflowing = hasAction && layoutResult.hasVisualOverflow - if (isTextOverflowing != isOverflowing) isTextOverflowing = isOverflowing - }, - ) + CompositionLocalProvider(LocalDensity provides fixedFontScaleDensity) { + Text( + text = snackbarMessage.message.resolveReference(), + modifier = Modifier.weight(1f, fill = false), + color = TangemTheme.colors2.text.neutral.secondary, + overflow = TextOverflow.Ellipsis, + maxLines = 2, + style = TangemTheme.typography2.captionMedium13, + onTextLayout = { layoutResult -> + val isOverflowing = hasAction && layoutResult.hasVisualOverflow + if (isTextOverflowing != isOverflowing) isTextOverflowing = isOverflowing + }, + ) + } SpacerW(TangemTheme.dimens2.x4) @@ -177,6 +180,7 @@ private fun Preview_TangemTopSnackbar_NoAction() { ) TangemTopSnackbar( snackbarMessage = SnackbarMessage( + startIconId = R.drawable.ic_eye_off_outline_24, message = stringReference( "Operation completed long long text that should be truncated with ellipsis at the end", ), 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/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/IsYieldBoostPromoEnabledForTokenUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/IsYieldBoostPromoEnabledForTokenUseCaseTest.kt index 9afbbb6492..f8f9929dd0 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/IsYieldBoostPromoEnabledForTokenUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/IsYieldBoostPromoEnabledForTokenUseCaseTest.kt @@ -7,11 +7,14 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.yield.supply.models.YieldBoostPromo import com.tangem.domain.yield.supply.models.YieldBoostStatus import com.tangem.domain.yield.supply.promo.YieldPromoRepository +import io.mockk.Deregisterable import io.mockk.coEvery import io.mockk.mockk +import io.mockk.registerInstanceFactory import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -25,11 +28,23 @@ class IsYieldBoostPromoEnabledForTokenUseCaseTest { private val contractAddress = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" private val networkRawId = "ethereum" + // Stub concrete instances of the sealed return types so MockK doesn't subclass them while recording coEvery + // (Objenesis on a JVM-sealed type throws InstantiationError flakily under full-suite CI runs). + private val instanceFactories = mutableListOf() + @BeforeEach fun setUp() { + instanceFactories += registerInstanceFactory { YieldBoostPromo.None } + instanceFactories += registerInstanceFactory { YieldBoostStatus.NotStarted } useCase = IsYieldBoostPromoEnabledForTokenUseCase(repository = repository) } + @AfterEach + fun tearDown() { + instanceFactories.forEach { it.deregister() } + instanceFactories.clear() + } + @Test fun `GIVEN currency is coin WHEN invoke THEN returns Right(false)`() = runTest { val coin = createCoin() diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/ShouldShowYieldBoostMainBannerUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/ShouldShowYieldBoostMainBannerUseCaseTest.kt index ae20808d28..72e4c6e0fa 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/ShouldShowYieldBoostMainBannerUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/promo/usecase/ShouldShowYieldBoostMainBannerUseCaseTest.kt @@ -5,11 +5,14 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.yield.supply.models.YieldBoostPromo import com.tangem.domain.yield.supply.models.YieldBoostStatus import com.tangem.domain.yield.supply.promo.YieldPromoRepository +import io.mockk.Deregisterable import io.mockk.coEvery import io.mockk.mockk +import io.mockk.registerInstanceFactory import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -23,11 +26,23 @@ class ShouldShowYieldBoostMainBannerUseCaseTest { private val contractAddress = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" private val networkRawId = "ethereum" + // Stub concrete instances of the sealed return types so MockK doesn't subclass them while recording coEvery + // (Objenesis on a JVM-sealed type throws InstantiationError flakily under full-suite CI runs). + private val instanceFactories = mutableListOf() + @BeforeEach fun setUp() { + instanceFactories += registerInstanceFactory { YieldBoostPromo.None } + instanceFactories += registerInstanceFactory { YieldBoostStatus.NotStarted } useCase = ShouldShowYieldBoostMainBannerUseCase(repository = repository) } + @AfterEach + fun tearDown() { + instanceFactories.forEach { it.deregister() } + instanceFactories.clear() + } + @Test fun `GIVEN promo repository throws WHEN invoke THEN returns Left`() = runTest { coEvery { repository.getYieldBoostPromo(userWalletId, false) } throws RuntimeException("net") 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/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index b598c30326..e808f05372 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -621,10 +621,11 @@ internal class StakingModel @Inject constructor( } override fun onAmountEnterClick() { - if (integration.preferredTargets.isEmpty()) { + val actionType = uiState.value.actionType + if (actionType !is StakingActionCommonType.Exit && integration.preferredTargets.isEmpty()) { messageSender.send(StakingAlertUM.noAvailableValidators()) } else { - if (uiState.value.actionType is StakingActionCommonType.Enter) { + if (actionType is StakingActionCommonType.Enter) { stateController.updateAll( ValidatorSelectChangeTransformer( selectedTarget = null, diff --git a/features/staking/impl/src/test/kotlin/com/tangem/features/staking/impl/presentation/model/StakingModelNavigationTest.kt b/features/staking/impl/src/test/kotlin/com/tangem/features/staking/impl/presentation/model/StakingModelNavigationTest.kt index 00d51a6c28..dde5946c11 100644 --- a/features/staking/impl/src/test/kotlin/com/tangem/features/staking/impl/presentation/model/StakingModelNavigationTest.kt +++ b/features/staking/impl/src/test/kotlin/com/tangem/features/staking/impl/presentation/model/StakingModelNavigationTest.kt @@ -354,6 +354,24 @@ internal class StakingModelNavigationTest : StakingModelTestBase() { model.onDestroy() } + @Test + fun `GIVEN empty preferredTargets AND Exit action WHEN onAmountEnterClick THEN no alert sent`() = runTest { + every { stateController.value } returns initialUiState + every { initialUiState.actionType } returns StakingActionCommonType.Exit(partiallyUnstakeDisabled = false) + every { testYield.preferredValidators } returns emptyList() + every { messageSender.send(any()) } just Runs + + val model = createModel(testScope = this) + advanceUntilIdle() + + model.onAmountEnterClick() + advanceUntilIdle() + + verify(exactly = 0) { messageSender.send(any()) } + + model.onDestroy() + } + @Test fun `GIVEN non-empty preferredTargets WHEN onAmountEnterClick THEN validator reset and onNextClick called`() = runTest { diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 5b41f9ef63..a1759b9e86 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -54,6 +54,7 @@ sealed interface SwapState { val isAccountsMode: Boolean, val isFeeCoverage: Boolean, val sendingAmount: BigDecimal, + val isSendingAmountLoading: Boolean = false, val currencyCheck: CryptoCurrencyCheck? = null, val validationResult: Throwable? = null, val minAdaValue: BigDecimal? = null, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImpl.kt index ff071510a7..03f6d275be 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImpl.kt @@ -40,7 +40,6 @@ import com.tangem.feature.swap.domain.fee.TransactionFeeResult import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.ui.SwapState import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo -import com.tangem.features.send.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkAndCalculateSubtractedAmount import com.tangem.features.send.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkFeeCoverage import com.tangem.features.swap.SwapFeatureToggles import com.tangem.utils.extensions.orZero @@ -107,7 +106,7 @@ class SwapTransferInteractorImpl @Inject constructor( fee = warningsFee, feeCurrencyBalanceAfterTransaction = null, ) - val (isFeeCoverage, sendingAmount) = getCoverageState( + val coverageState = getCoverageState( fromTokenInfo = fromTokenInfo, userWallet = userWallet, fee = fee, @@ -130,8 +129,9 @@ class SwapTransferInteractorImpl @Inject constructor( appCurrency = appCurrency, isBalanceHidden = isBalanceHidden, isAccountsMode = isAccountsMode, - isFeeCoverage = isFeeCoverage, - sendingAmount = sendingAmount, + isFeeCoverage = coverageState.isFeeCoverage, + sendingAmount = coverageState.sendingAmount, + isSendingAmountLoading = coverageState.isSendingAmountLoading, currencyCheck = currencyCheck, ) } @@ -155,7 +155,7 @@ class SwapTransferInteractorImpl @Inject constructor( userWallet: UserWallet, fee: Fee?, currencyCheck: CryptoCurrencyCheck, - ): Pair { + ): CoverageState { val swapCurrencyStatus = fromTokenInfo.swapCurrencyStatus val isAmountSubtractAvailable = isAmountSubtractAvailable( userWalletId = userWallet.walletId, @@ -173,16 +173,29 @@ class SwapTransferInteractorImpl @Inject constructor( feeValue = feeValue, reduceAmountBy = reduceAmountBy, ) - val sendingAmount = checkAndCalculateSubtractedAmount( - isAmountSubtractAvailable = isAmountSubtractAvailable, - cryptoCurrencyStatus = fromTokenInfo.swapCurrencyStatus.status, - amountValue = amount.value, - feeValue = feeValue, - reduceAmountBy = reduceAmountBy, + // When fee coverage applies, the entered amount can't be sent together with the fee, so the + // sent (and therefore received) amount is the entered amount reduced by the fee. This tracks the + // input: as the user edits the amount, the received amount changes with it. + val sendingAmount = if (isFeeCoverage) { + (amount.value - feeValue).coerceAtLeast(BigDecimal.ZERO) + } else { + amount.value + } + // While subtraction is possible but the fee has not loaded yet, the final received amount + // (entered - fee) is unknown, so it must be shown as loading instead of the un-subtracted value. + return CoverageState( + isFeeCoverage = isFeeCoverage, + sendingAmount = sendingAmount, + isSendingAmountLoading = fee == null && isAmountSubtractAvailable, ) - return isFeeCoverage to sendingAmount } + private data class CoverageState( + val isFeeCoverage: Boolean, + val sendingAmount: BigDecimal, + val isSendingAmountLoading: Boolean, + ) + private suspend fun isAmountSubtractAvailable( userWalletId: UserWalletId, currency: CryptoCurrency, diff --git a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImplTest.kt b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImplTest.kt index fc31a2c1ef..cdecb5a391 100644 --- a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImplTest.kt +++ b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImplTest.kt @@ -299,7 +299,7 @@ internal class SwapTransferInteractorImplTest { } returns true.right() // entered amount = full balance → balance < amount + fee, balance > fee, balance >= amount - // → isFeeCoverage = true, sendingAmount = balance - fee + // → isFeeCoverage = true, sendingAmount = entered - fee (== balance - fee at max) val result = sut.updateTransfer( fromSwapCurrencyStatus = fromCurrencyStatus, toSwapCurrencyStatus = toCurrencyStatus, @@ -310,6 +310,113 @@ internal class SwapTransferInteractorImplTest { assertThat(result.isFeeCoverage).isTrue() assertThat(result.sendingAmount).isEqualTo(balance - feeValue) + assertThat(result.isSendingAmountLoading).isFalse() + } + + @Test + fun `GIVEN subtract available and sub-max amount in coverage zone WHEN updateTransfer THEN sendingAmount is entered minus fee`() = + runTest { + val appCurrency = AppCurrency(code = "USD", name = "US Dollar", symbol = "$") + val userWallet: UserWallet = mockk(relaxed = true) + val balance = BigDecimal("1.5") + val feeValue = BigDecimal("0.2") + // entered is below the balance but still within one fee of it → coverage applies, yet the + // received amount must track the entered amount (entered - fee), not clamp to balance - fee. + val enteredAmount = BigDecimal("1.45") + val fromCurrencyStatus = buildCurrencyStatus( + rawCurrencyId = FROM_RAW_CURRENCY_ID, + decimals = FROM_DECIMALS, + fiatRate = BigDecimal.TEN, + amount = balance, + userWallet = userWallet, + ) + val toCurrencyStatus = buildCurrencyStatus( + rawCurrencyId = TO_RAW_CURRENCY_ID, + decimals = TO_DECIMALS, + userWallet = userWallet, + ) + val fee: Fee = mockk(relaxed = true) { + every { amount.value } returns feeValue + } + every { getSelectedAppCurrencyUseCase() } returns flowOf(appCurrency.right()) + every { getBalanceHidingSettingsUseCase.isBalanceHidden() } returns flowOf(false) + coEvery { isAccountsModeEnabledUseCase.invokeSync() } returns false + coEvery { + getCurrencyCheckUseCase( + userWalletId = any(), + currencyStatus = any(), + feeCurrencyStatus = any(), + amount = any(), + fee = any(), + feeCurrencyBalanceAfterTransaction = any(), + recipientAddress = any(), + ) + } returns buildCurrencyCheck() + coEvery { + isAmountSubtractAvailableUseCase(any(), any(), any()) + } returns true.right() + + val result = sut.updateTransfer( + fromSwapCurrencyStatus = fromCurrencyStatus, + toSwapCurrencyStatus = toCurrencyStatus, + fromTokenAmount = enteredAmount.toPlainString(), + feePaidCurrencyStatus = null, + fee = fee, + ) as SwapState.Transfer + + assertThat(result.isFeeCoverage).isTrue() + assertThat(result.sendingAmount).isEqualTo(enteredAmount - feeValue) + // it must NOT clamp to balance - fee + assertThat(result.sendingAmount).isNotEqualTo(balance - feeValue) + } + + @Test + fun `GIVEN subtract available but fee not loaded yet WHEN updateTransfer THEN isSendingAmountLoading is true`() = + runTest { + val appCurrency = AppCurrency(code = "USD", name = "US Dollar", symbol = "$") + val userWallet: UserWallet = mockk(relaxed = true) + val balance = BigDecimal("1.5") + val fromCurrencyStatus = buildCurrencyStatus( + rawCurrencyId = FROM_RAW_CURRENCY_ID, + decimals = FROM_DECIMALS, + fiatRate = BigDecimal.TEN, + amount = balance, + userWallet = userWallet, + ) + val toCurrencyStatus = buildCurrencyStatus( + rawCurrencyId = TO_RAW_CURRENCY_ID, + decimals = TO_DECIMALS, + userWallet = userWallet, + ) + every { getSelectedAppCurrencyUseCase() } returns flowOf(appCurrency.right()) + every { getBalanceHidingSettingsUseCase.isBalanceHidden() } returns flowOf(false) + coEvery { isAccountsModeEnabledUseCase.invokeSync() } returns false + coEvery { + getCurrencyCheckUseCase( + userWalletId = any(), + currencyStatus = any(), + feeCurrencyStatus = any(), + amount = any(), + fee = any(), + feeCurrencyBalanceAfterTransaction = any(), + recipientAddress = any(), + ) + } returns buildCurrencyCheck() + coEvery { + isAmountSubtractAvailableUseCase(any(), any(), any()) + } returns true.right() + + // subtraction is possible but the fee has not loaded yet (fee = null) → the received amount + // depends on the fee, so it can't be known yet and must be reported as loading. + val result = sut.updateTransfer( + fromSwapCurrencyStatus = fromCurrencyStatus, + toSwapCurrencyStatus = toCurrencyStatus, + fromTokenAmount = balance.toPlainString(), + feePaidCurrencyStatus = null, + fee = null, + ) as SwapState.Transfer + + assertThat(result.isSendingAmountLoading).isTrue() } // endregion diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilder.kt index 6afae71a25..10a023634f 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilder.kt @@ -21,6 +21,7 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.simple import com.tangem.core.ui.utils.parseBigDecimalOrNull import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.account.Account @@ -31,11 +32,11 @@ import com.tangem.feature.swap.domain.models.ui.SwapState import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo import com.tangem.feature.swap.model.SwapProcessDataState import com.tangem.feature.swap.models.* -import com.tangem.feature.swap.ui.SwapAmountScreenClickIntents -import com.tangem.feature.swap.ui.swapSuccessNavigation import com.tangem.feature.swap.models.SwapButton.Mode import com.tangem.feature.swap.models.states.SwapNotificationUM import com.tangem.feature.swap.presentation.R +import com.tangem.feature.swap.ui.SwapAmountScreenClickIntents +import com.tangem.feature.swap.ui.swapSuccessNavigation import com.tangem.features.send.api.utils.formatFooterFiatFee import com.tangem.features.send.api.utils.getTronTokenFeeSendingText import com.tangem.utils.extensions.orZero @@ -59,11 +60,9 @@ internal class SwapTransferStateBuilder @Inject constructor( fee: Fee?, ): SwapStateHolder { val fromTokenSwapInfo = transferState.fromTokenInfo - val toTokenSwapInfo = transferState.toTokenInfo val isInsufficientBalance = transferState.isInsufficientBalance val prevSendCard = uiStateHolder.sendCardData as? SwapCardState.SwapCardData val prevAmountField = prevSendCard?.amountField - val displayValue = prevAmountField?.value.orEmpty() val notifications = notificationsFactory.getNotifications( transferState = transferState, feeCryptoCurrencyStatus = feePaidCryptoCurrencyStatus, @@ -75,25 +74,14 @@ internal class SwapTransferStateBuilder @Inject constructor( return uiStateHolder.copy( sendCardData = createSendSwapCardState( actions = actions, - displayValue = displayValue, tokenSwapInfo = fromTokenSwapInfo, appCurrency = transferState.appCurrency, isAccountsMode = transferState.isAccountsMode, - isFromCard = true, isBalanceHidden = transferState.isBalanceHidden, isInsufficientBalance = isInsufficientBalance, prevAmountField = prevAmountField, ), - receiveCardData = createSendSwapCardState( - actions = actions, - displayValue = displayValue, - tokenSwapInfo = toTokenSwapInfo, - appCurrency = transferState.appCurrency, - isAccountsMode = transferState.isAccountsMode, - isFromCard = false, - isBalanceHidden = transferState.isBalanceHidden, - isInsufficientBalance = isInsufficientBalance, - ), + receiveCardData = createReceiveCard(actions = actions, transferState = transferState), isInsufficientFunds = isInsufficientBalance, swapButton = SwapButton( walletInteractionIcon = walletInterationIcon(transferState.userWallet), @@ -109,14 +97,12 @@ internal class SwapTransferStateBuilder @Inject constructor( @Suppress("LongParameterList") private fun createSendSwapCardState( actions: UiActions, - displayValue: String, tokenSwapInfo: TokenSwapInfo, appCurrency: AppCurrency, isAccountsMode: Boolean, - isFromCard: Boolean, isBalanceHidden: Boolean, isInsufficientBalance: Boolean, - prevAmountField: AmountFieldModel? = null, + prevAmountField: AmountFieldModel?, ): SwapCardState { val swapCurrencyStatus = tokenSwapInfo.swapCurrencyStatus val currency = swapCurrencyStatus.currency @@ -126,7 +112,7 @@ internal class SwapTransferStateBuilder @Inject constructor( actions = actions, swapCurrencyStatus = tokenSwapInfo.swapCurrencyStatus, isAccountsMode = isAccountsMode, - isFromCard = isFromCard, + isFromCard = true, isInsufficientBalance = isInsufficientBalance, ), currencyIconState = iconConverter.convert( @@ -140,18 +126,61 @@ internal class SwapTransferStateBuilder @Inject constructor( balance = swapCurrencyStatus.status.getFormattedAmount(), isBalanceHidden = isBalanceHidden, appCurrency = appCurrency, - amountField = if (isFromCard) { - buildAmountField( - actions = actions, - prevAmountField = prevAmountField, - swapCurrencyStatus = swapCurrencyStatus, - appCurrency = appCurrency, - ) + amountField = buildAmountField( + actions = actions, + prevAmountField = prevAmountField, + swapCurrencyStatus = swapCurrencyStatus, + appCurrency = appCurrency, + ), + ) + } + + /** + * Builds the read-only receive card from [SwapState.Transfer.sendingAmount] — the amount that will + * actually be received, already reduced by the fee when fee coverage applies. While the reduced amount + * is not yet known (fee still loading) the amount and fiat fields are null, which makes the read-only + * card render a shimmer instead of the un-subtracted value. + */ + private fun createReceiveCard(actions: UiActions, transferState: SwapState.Transfer): SwapCardState { + val toTokenSwapInfo = transferState.toTokenInfo + val swapCurrencyStatus = toTokenSwapInfo.swapCurrencyStatus + val currency = swapCurrencyStatus.currency + val appCurrency = transferState.appCurrency + val sendingAmount = transferState.sendingAmount + // No reduction can happen when the balance is insufficient (fee coverage requires balance >= amount), + // so there is nothing to wait for — show the amount instead of a shimmer. + val isLoading = transferState.isSendingAmountLoading && !transferState.isInsufficientBalance + val fiatRate = swapCurrencyStatus.status.value.fiatRate + + return SwapCardState.SwapCardData( + type = createSendTransactionCardType( + actions = actions, + swapCurrencyStatus = swapCurrencyStatus, + isAccountsMode = transferState.isAccountsMode, + isFromCard = false, + isInsufficientBalance = transferState.isInsufficientBalance, + ), + currencyIconState = iconConverter.convert( + value = swapCurrencyStatus.status, + ), + tokenSymbol = stringReference(currency.symbol), + amountEquivalent = if (isLoading) { + null } else { - // Read-only receive card mirrors the same display value the "from" card shows in transfer mode. + getFormattedFiatAmount(appCurrency = appCurrency, amount = fiatRate?.multiply(sendingAmount)) + }, + balance = swapCurrencyStatus.status.getFormattedAmount(), + isBalanceHidden = transferState.isBalanceHidden, + appCurrency = appCurrency, + amountField = if (isLoading) { + null + } else { + val value = sendingAmount.format { + simple(decimals = currency.decimals) + } displayAmountField( actions = actions, - value = displayValue, + value = value, swapCurrencyStatus = swapCurrencyStatus, appCurrency = appCurrency, ) @@ -324,6 +353,10 @@ internal class SwapTransferStateBuilder @Inject constructor( ) return uiStateHolder.copy( notifications = notifications, + // Rebuild the receive card from the refreshed transferState: this path runs after the fee + // selector resolves, when sendingAmount may have just been reduced by the fee. Only the + // receive card is rebuilt to avoid clobbering the user's in-progress input on the "from" card. + receiveCardData = createReceiveCard(actions = actions, transferState = transferState), swapButton = uiStateHolder.swapButton.copy( isEnabled = getTransferButtonEnabled(notifications, fee, isTangemPayWithdrawal), ), diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilderTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilderTest.kt index 30cb7b21ba..a5d4203e98 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilderTest.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilderTest.kt @@ -22,6 +22,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fee import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.account.Account import com.tangem.domain.models.network.Network @@ -371,6 +372,123 @@ internal class SwapTransferStateBuilderTest { } } + @Test + fun `GIVEN fee coverage reduces amount WHEN createTransferState THEN receive card shows reduced sendingAmount`() = + runTest { + // entered 1.5, but fee coverage reduces the received (sending) amount to 1.3 + val sendingAmount = BigDecimal("1.3") + val transferState = buildTransferState( + fromAmount = BigDecimal("1.5"), + toAmount = sendingAmount, + isAccountsMode = false, + isFeeCoverage = true, + ) + + val result = sut.createTransferState( + actions = actions, + transferState = transferState, + uiStateHolder = baseStateHolder(), + feePaidCryptoCurrencyStatus = null, + fee = mockk(relaxed = true), + ) + + val sendCard = result.sendCardData as SwapCardState.SwapCardData + val receiveCard = result.receiveCardData as SwapCardState.SwapCardData + val expectedFiat = stringReference( + toCurrencyStatus.status.value.fiatRate!!.multiply(sendingAmount).format { + fiat(fiatCurrencyCode = AppCurrency.Default.code, fiatCurrencySymbol = AppCurrency.Default.symbol) + }, + ) + // the "from" card keeps the user's typed value, the receive card shows the reduced amount + its fiat + assertThat(sendCard.amountField?.value).isEqualTo(initialAmountValue) + assertThat(receiveCard.amountField?.value).isEqualTo( + sendingAmount.parseBigDecimal(transferState.toTokenInfo.tokenAmount.decimals), + ) + assertThat(receiveCard.amountEquivalent).isEqualTo(expectedFiat) + } + + @Test + fun `GIVEN sendingAmount loading WHEN createTransferState THEN receive card amount and fiat shimmer`() = + runTest { + // fee not loaded yet → reduced amount unknown → receive card shimmers (null amount + null fiat) + val transferState = buildTransferState( + fromAmount = BigDecimal("1.5"), + toAmount = BigDecimal("1.5"), + isAccountsMode = false, + isSendingAmountLoading = true, + ) + + val result = sut.createTransferState( + actions = actions, + transferState = transferState, + uiStateHolder = baseStateHolder(), + feePaidCryptoCurrencyStatus = null, + fee = null, + ) + + val receiveCard = result.receiveCardData as SwapCardState.SwapCardData + assertThat(receiveCard.amountField).isNull() + assertThat(receiveCard.amountEquivalent).isNull() + } + + @Test + fun `GIVEN sendingAmount loading but insufficient balance WHEN createTransferState THEN receive card shows amount not shimmer`() = + runTest { + // Insufficient balance → fee coverage can't apply, so there is no reduction to wait for. + // The receive card must show the amount instead of shimmering. + val amount = BigDecimal("99") + val transferState = buildTransferState( + fromAmount = amount, + toAmount = amount, + isAccountsMode = false, + isInsufficientBalance = true, + isSendingAmountLoading = true, + ) + + val result = sut.createTransferState( + actions = actions, + transferState = transferState, + uiStateHolder = baseStateHolder(), + feePaidCryptoCurrencyStatus = null, + fee = null, + ) + + val receiveCard = result.receiveCardData as SwapCardState.SwapCardData + assertThat(receiveCard.amountField?.value).isEqualTo( + amount.parseBigDecimal(transferState.toTokenInfo.tokenAmount.decimals), + ) + assertThat(receiveCard.amountEquivalent).isNotNull() + } + + @Test + fun `GIVEN reduced sendingAmount WHEN updateTransferButtonEnableState THEN receive card is rebuilt to reduced amount`() = + runTest { + // After the fee resolves, sendingAmount is reduced; the refresh path must rebuild the receive card + // so it no longer shows the stale full amount. + val sendingAmount = BigDecimal("1.3") + val transferState = buildTransferState( + fromAmount = BigDecimal("1.5"), + toAmount = sendingAmount, + isAccountsMode = false, + isFeeCoverage = true, + ) + + val result = sut.updateTransferButtonEnableState( + dataState = SwapProcessDataState(), + transferState = transferState, + actions = actions, + uiStateHolder = baseStateHolder(), + feePaidCryptoCurrencyStatus = null, + fee = mockk(relaxed = true), + isTangemPayWithdrawal = false, + ) + + val receiveCard = result.receiveCardData as SwapCardState.SwapCardData + assertThat(receiveCard.amountField?.value).isEqualTo( + sendingAmount.parseBigDecimal(transferState.toTokenInfo.tokenAmount.decimals), + ) + } + @Test fun `GIVEN Tron fee WHEN updateTransferButtonEnableState THEN transferFooter uses Tron token fee sending text`() = runTest { @@ -747,8 +865,12 @@ internal class SwapTransferStateBuilderTest { ) { val sendCard = result.sendCardData as SwapCardState.SwapCardData val receiveCard = result.receiveCardData as SwapCardState.SwapCardData + // The "from" card preserves the user's typed value; the receive card shows the (possibly + // fee-reduced) sendingAmount formatted with the receive token's decimals. assertThat(sendCard.amountField?.value).isEqualTo(initialAmountValue) - assertThat(receiveCard.amountField?.value).isEqualTo(initialAmountValue) + assertThat(receiveCard.amountField?.value).isEqualTo( + transferState.sendingAmount.parseBigDecimal(transferState.toTokenInfo.tokenAmount.decimals), + ) assertThat(sendCard.currencyIconState).isEqualTo(fromIcon) assertThat(receiveCard.currencyIconState).isEqualTo(toIcon) assertThat(sendCard.isBalanceHidden).isEqualTo(transferState.isBalanceHidden) @@ -778,6 +900,8 @@ internal class SwapTransferStateBuilderTest { toAmount: BigDecimal, isAccountsMode: Boolean, isInsufficientBalance: Boolean = false, + isFeeCoverage: Boolean = false, + isSendingAmountLoading: Boolean = false, ): SwapState.Transfer { val fromInfo = TokenSwapInfo( tokenAmount = SwapAmount(value = fromAmount, decimals = fromCurrencyStatus.currency.decimals), @@ -798,8 +922,9 @@ internal class SwapTransferStateBuilderTest { appCurrency = AppCurrency.Default, isBalanceHidden = false, isAccountsMode = isAccountsMode, - isFeeCoverage = false, + isFeeCoverage = isFeeCoverage, sendingAmount = toAmount, + isSendingAmountLoading = isSendingAmountLoading, ) } 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/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index d86bc04a23..df605c6c7e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -533,7 +533,7 @@ internal class TokenDetailsModel @Inject constructor( network = cryptoCurrency.network, ).getOrElse { false } - val isSupported = isXPUBSupported() + val isSupported = isXpubSupported() val isDynamicAddressesAvailable = isSupported && isDynamicAddressesAvailableUseCase(userWallet, cryptoCurrency) @@ -546,7 +546,7 @@ internal class TokenDetailsModel @Inject constructor( } } - private suspend fun isXPUBSupported(): Boolean { + private suspend fun isXpubSupported(): Boolean { return isXpubSupportedUseCase(userWalletId = userWalletId, network = cryptoCurrency.network) } @@ -853,7 +853,6 @@ internal class TokenDetailsModel @Inject constructor( if (handleUnavailabilityReason(unavailabilityReason = unavailabilityReason)) { return } - if (isTopUpBlockedByBackupError()) return modelScope.launch { if (checkYieldSupply && needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus)) { @@ -1438,15 +1437,17 @@ internal class TokenDetailsModel @Inject constructor( network = cryptoCurrency.network, ).getOrElse { false } - val isSupported = isXPUBSupported() + val isXpubSupported = isXpubSupported() + val isDynamicAddressesAvailable = isXpubSupported && + isDynamicAddressesAvailableUseCase(userWallet, cryptoCurrency) redesignStateController.update( UpdateTopBarMenuTransformer( userWallet = userWallet, hasDerivations = hasDerivations, - isXPubSupported = isSupported, - onGenerateExtendedKey = ::onGenerateExtendedKey, - onHideClick = ::onHideClick, + isXpubSupported = isXpubSupported, + isDynamicAddressesAvailable = isDynamicAddressesAvailable, + clickIntents = this@TokenDetailsModel, ), ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt index 789ddc5ec5..f7b398c873 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt @@ -18,6 +18,7 @@ internal sealed interface StakingBlockUM { val cryptoAmount: BigDecimal?, val fiatAmount: BigDecimal?, val onStakeClicked: () -> Unit, + val isBetaMode: Boolean = false, ) : StakingBlockUM data class StakeAvailable( @@ -26,5 +27,6 @@ internal sealed interface StakingBlockUM { val subtitleText: TextReference, val isEnabled: Boolean, val onStakeClicked: () -> Unit, + val isBetaMode: Boolean = false, ) : StakingBlockUM } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt index 845d22c4e1..5c6019e55d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt @@ -15,6 +15,8 @@ import com.tangem.domain.models.staking.RewardBlockType import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.staking.model.StakingIntegrationID +import com.tangem.domain.staking.model.optionOrNull import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.IconState import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM @@ -53,12 +55,20 @@ internal class TokenDetailsStakingInfoConverter( StakingAvailability.TemporaryUnavailable -> StakingBlockUM.TemporaryUnavailable StakingAvailability.Unavailable -> null is StakingAvailability.Full -> getStakedBlockOrNull(status) - is StakingAvailability.Available -> getStakingInfoBlock(status, state) + is StakingAvailability.Available -> getStakingInfoBlock( + status = status, + state = state, + isBetaMode = stakingAvailability.optionOrNull?.integrationId == StakingIntegrationID.P2PEthPool, + ) } } @Suppress("CyclomaticComplexMethod") - private fun getStakingInfoBlock(status: CryptoCurrencyStatus, state: TokenDetailsState): StakingBlockUM? { + private fun getStakingInfoBlock( + status: CryptoCurrencyStatus, + state: TokenDetailsState, + isBetaMode: Boolean, + ): StakingBlockUM? { val stakingBalance = status.value.stakingBalance as? StakingBalance.Data val stakingCryptoAmount = stakingBalance?.getTotalStakingBalance(status.currency.network.rawId) @@ -88,7 +98,12 @@ internal class TokenDetailsStakingInfoConverter( return when { stakingCryptoAmount.isNullOrZero() && stakingEntryInfo != null -> { if (!hasPendingBalances) { - getStakeAvailableState(stakingEntryInfo, iconState, isStakingButtonEnabled(status)) + getStakeAvailableState( + stakingEntryInfo = stakingEntryInfo, + iconState = iconState, + isEnabled = isStakingButtonEnabled(status), + isBetaMode = isBetaMode, + ) } else { getStakedBlockWithFiatAmount(status, pendingAmount, null) } @@ -165,6 +180,7 @@ internal class TokenDetailsStakingInfoConverter( stakingEntryInfo: StakingEntryInfo, iconState: IconState, isEnabled: Boolean, + isBetaMode: Boolean, ): StakingBlockUM.StakeAvailable { return StakingBlockUM.StakeAvailable( titleText = resourceReference(id = R.string.token_details_staking_block_title), @@ -175,6 +191,7 @@ internal class TokenDetailsStakingInfoConverter( iconState = iconState, isEnabled = isEnabled, onStakeClicked = clickIntents::onStakeBannerClick, + isBetaMode = isBetaMode, ) } @@ -205,6 +222,7 @@ internal class TokenDetailsStakingInfoConverter( ), rewardValue = getRewardText(status, stakingRewardAmount), onStakeClicked = clickIntents::onStakeBannerClick, + isBetaMode = status.value.stakingBalance is StakingBalance.Data.P2PEthPool, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformer.kt index 320e28bdce..682927afcf 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformer.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformer.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.themedColor import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM import com.tangem.features.tokendetails.impl.R import com.tangem.utils.transformer.Transformer @@ -15,9 +16,9 @@ import kotlinx.collections.immutable.toImmutableList internal class UpdateTopBarMenuTransformer( private val userWallet: UserWallet, private val hasDerivations: Boolean, - private val isXPubSupported: Boolean, - private val onGenerateExtendedKey: () -> Unit, - private val onHideClick: () -> Unit, + private val isXpubSupported: Boolean, + private val isDynamicAddressesAvailable: Boolean, + private val clickIntents: TokenDetailsClickIntents, ) : Transformer { override fun transform(prevState: TokenDetailsUM): TokenDetailsUM = prevState.copy( @@ -30,12 +31,21 @@ internal class UpdateTopBarMenuTransformer( persistentListOf() } else { buildList { - if (isXPubSupported && hasDerivations) { + if (isDynamicAddressesAvailable) { + add( + TangemDropdownMenuItem( + title = resourceReference(R.string.dynamic_addresses), + textColor = themedColor { TangemTheme.colors.text.primary1 }, + onClick = clickIntents::onDynamicAddressesClick, + ), + ) + } + if (isXpubSupported && hasDerivations) { add( TangemDropdownMenuItem( title = resourceReference(R.string.token_details_generate_xpub), textColor = themedColor { TangemTheme.colors.text.primary1 }, - onClick = onGenerateExtendedKey, + onClick = clickIntents::onGenerateExtendedKey, ), ) } @@ -43,7 +53,7 @@ internal class UpdateTopBarMenuTransformer( TangemDropdownMenuItem( title = resourceReference(R.string.token_details_hide_token), textColor = themedColor { TangemTheme.colors.text.warning }, - onClick = onHideClick, + onClick = clickIntents::onHideClick, ), ) }.toImmutableList() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt index d077ce9d52..4ec33c934f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt @@ -40,10 +40,8 @@ internal fun StakingBalanceBlock( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), modifier = Modifier.weight(1f), ) { - Text( - text = stringResourceSafe(R.string.staking_native), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, + StakingBalanceTitleRow( + isBetaMode = state.isBetaMode, modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing2), ) Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { @@ -84,6 +82,32 @@ internal fun StakingBalanceBlock( } } +@Composable +private fun StakingBalanceTitleRow(isBetaMode: Boolean, modifier: Modifier = Modifier) { + Row( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + modifier = modifier, + ) { + Text( + text = stringResourceSafe(R.string.staking_native), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + if (isBetaMode) { + Text( + text = StringsSigns.DOT, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + Text( + text = stringResourceSafe(R.string.beta_mode_warning_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + } + } +} + // region Preview @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlockLegacy.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlockLegacy.kt index 4b78546b82..bac99ec38b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlockLegacy.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlockLegacy.kt @@ -33,6 +33,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPre import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.CurrencyIcon import com.tangem.features.tokendetails.impl.R +import com.tangem.utils.StringsSigns /** * Token staking block @@ -99,12 +100,26 @@ private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifi ) SpacerW8() Column { - Text( - text = state.titleText.resolveReference(), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle2, - modifier = Modifier.testTag(TokenDetailsScreenTestTags.STAKING_SERVICE_TITLE), - ) + Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) { + Text( + text = state.titleText.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle2, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.STAKING_SERVICE_TITLE), + ) + if (state.isBetaMode) { + Text( + text = StringsSigns.DOT, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle2, + ) + Text( + text = stringResourceSafe(R.string.beta_mode_warning_title), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle2, + ) + } + } Spacer(modifier = Modifier.size(TangemTheme.dimens.size4)) diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformerTest.kt index d66631ccae..3c53e0f255 100644 --- a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformerTest.kt +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformerTest.kt @@ -7,6 +7,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM @@ -29,8 +30,7 @@ class UpdateTopBarMenuTransformerTest { private val coldWallet: UserWallet.Cold = mockk(relaxed = true) private val hotWallet: UserWallet.Hot = mockk(relaxed = true) private val cardTypesResolver: CardTypesResolver = mockk(relaxed = true) - private val onGenerateExtendedKey: () -> Unit = mockk(relaxed = true) - private val onHideClick: () -> Unit = mockk(relaxed = true) + private val clickIntents: TokenDetailsClickIntents = mockk(relaxed = true) @BeforeEach fun setUp() { @@ -77,8 +77,8 @@ class UpdateTopBarMenuTransformerTest { assertThat(result.topAppBarUM.menuItems).hasSize(1) result.topAppBarUM.menuItems.single().onClick() - verify(exactly = 1) { onHideClick.invoke() } - verify(exactly = 0) { onGenerateExtendedKey.invoke() } + verify(exactly = 1) { clickIntents.onHideClick() } + verify(exactly = 0) { clickIntents.onGenerateExtendedKey() } } @Test @@ -169,6 +169,48 @@ class UpdateTopBarMenuTransformerTest { assertThat(result.marketPriceBlockState).isSameInstanceAs(state.marketPriceBlockState) } + @Test + fun `GIVEN dynamic addresses available WHEN transform THEN dynamic addresses item is shown first`() { + // GIVEN + every { cardTypesResolver.isSingleWalletWithToken() } returns false + val transformer = createTransformer( + userWallet = coldWallet, + hasDerivations = false, + isXPubSupported = false, + isDynamicAddressesAvailable = true, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN — Dynamic addresses item first, Hide token second + assertThat(result.topAppBarUM.menuItems).hasSize(2) + + result.topAppBarUM.menuItems.first().onClick() + verify(exactly = 1) { clickIntents.onDynamicAddressesClick() } + } + + @Test + fun `GIVEN dynamic addresses unavailable WHEN transform THEN dynamic addresses item is hidden`() { + // GIVEN + every { cardTypesResolver.isSingleWalletWithToken() } returns false + val transformer = createTransformer( + userWallet = coldWallet, + hasDerivations = false, + isXPubSupported = false, + isDynamicAddressesAvailable = false, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN — only Hide token + assertThat(result.topAppBarUM.menuItems).hasSize(1) + + result.topAppBarUM.menuItems.single().onClick() + verify(exactly = 0) { clickIntents.onDynamicAddressesClick() } + } + @Test fun `GIVEN callbacks WHEN menu items invoked THEN callbacks are dispatched`() { // GIVEN @@ -177,6 +219,7 @@ class UpdateTopBarMenuTransformerTest { userWallet = coldWallet, hasDerivations = true, isXPubSupported = true, + isDynamicAddressesAvailable = true, ) // WHEN @@ -184,20 +227,22 @@ class UpdateTopBarMenuTransformerTest { result.topAppBarUM.menuItems.forEach { it.onClick() } // THEN - verify(exactly = 1) { onGenerateExtendedKey.invoke() } - verify(exactly = 1) { onHideClick.invoke() } + verify(exactly = 1) { clickIntents.onDynamicAddressesClick() } + verify(exactly = 1) { clickIntents.onGenerateExtendedKey() } + verify(exactly = 1) { clickIntents.onHideClick() } } private fun createTransformer( userWallet: UserWallet, hasDerivations: Boolean, isXPubSupported: Boolean, + isDynamicAddressesAvailable: Boolean = false, ) = UpdateTopBarMenuTransformer( userWallet = userWallet, hasDerivations = hasDerivations, - isXPubSupported = isXPubSupported, - onGenerateExtendedKey = onGenerateExtendedKey, - onHideClick = onHideClick, + isXpubSupported = isXPubSupported, + isDynamicAddressesAvailable = isDynamicAddressesAvailable, + clickIntents = clickIntents, ) private fun initialState(): TokenDetailsUM = TokenDetailsUM( 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/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt index 1b19f874c0..b2cc823dbd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt @@ -373,7 +373,6 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( ) if (handleUnavailabilityReason(unavailabilityReason)) return - if (isTopUpBlockedByBackupError(accountId.userWalletId)) return modelScope.launch(dispatchers.main) { if (needShowYieldSupplyWarning(cryptoCurrencyStatus)) { @@ -462,7 +461,6 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } override fun onMultiWalletSwapClick(userWalletId: UserWalletId) { - if (isTopUpBlockedByBackupError(userWalletId)) return if (!isMultiWalletTokensLoaded()) return modelScope.launch { 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/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 6aaa3d0243..b298226c5c 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-6.0-1580" +tangemBlockchainSdk = "releases-6.0-1587" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "releases-6.0-626" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ 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\$")) }