Updated on 2026-08-14
This commit is contained in:
commit
98a1453d82
40 changed files with 810 additions and 207 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@
|
|||
},
|
||||
{
|
||||
"name": "TWI_1377_MANAGE_FUNDS",
|
||||
"version": "5.40"
|
||||
"version": "6.0"
|
||||
},
|
||||
{
|
||||
"name": "AND_15009_SWAP_PROVIDER_FILTER_ENABLED",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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<Deregisterable>()
|
||||
|
||||
@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()
|
||||
|
|
|
|||
|
|
@ -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<Deregisterable>()
|
||||
|
||||
@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")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.SubcomposeLayout
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.lerp
|
||||
import com.tangem.core.ui.components.SpacerH8
|
||||
|
|
@ -36,7 +37,7 @@ import kotlin.math.ceil
|
|||
import kotlin.math.floor
|
||||
|
||||
@Composable
|
||||
internal fun PromoBannersBlock(state: PromoBannersBlockUM, modifier: Modifier = Modifier) {
|
||||
internal fun PromoBannersBlock(state: PromoBannersBlockUM, horizontalItemPadding: Dp, modifier: Modifier = Modifier) {
|
||||
if (state.banners.isEmpty()) return
|
||||
|
||||
val containerColor = bannerContainerColor(state.placeholder)
|
||||
|
|
@ -51,13 +52,14 @@ internal fun PromoBannersBlock(state: PromoBannersBlockUM, modifier: Modifier =
|
|||
SingleBanner(
|
||||
banner = banner,
|
||||
containerColor = containerColor,
|
||||
modifier = modifier,
|
||||
modifier = modifier.padding(horizontal = horizontalItemPadding),
|
||||
)
|
||||
} else {
|
||||
key(state.userWalletId) {
|
||||
BannersCarousel(
|
||||
state = state,
|
||||
containerColor = containerColor,
|
||||
horizontalPadding = horizontalItemPadding,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
@ -109,7 +111,12 @@ private fun SingleBanner(banner: PromoBannerNotificationUM, containerColor: Colo
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun BannersCarousel(state: PromoBannersBlockUM, containerColor: Color, modifier: Modifier = Modifier) {
|
||||
private fun BannersCarousel(
|
||||
state: PromoBannersBlockUM,
|
||||
containerColor: Color,
|
||||
horizontalPadding: Dp,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = state.initialPage,
|
||||
pageCount = { state.banners.size },
|
||||
|
|
@ -139,6 +146,7 @@ private fun BannersCarousel(state: PromoBannersBlockUM, containerColor: Color, m
|
|||
banners = state.banners,
|
||||
pagerState = pagerState,
|
||||
containerColor = containerColor,
|
||||
horizontalPadding = horizontalPadding,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
|
|
@ -163,6 +171,7 @@ private fun SmoothHeightPager(
|
|||
banners: List<PromoBannerNotificationUM>,
|
||||
pagerState: PagerState,
|
||||
containerColor: Color,
|
||||
horizontalPadding: Dp,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
SubcomposeLayout(modifier = modifier) { constraints ->
|
||||
|
|
@ -206,6 +215,7 @@ private fun SmoothHeightPager(
|
|||
) { page ->
|
||||
val banner = banners.getOrNull(page) ?: return@HorizontalPager
|
||||
BannerNotification(
|
||||
modifier = Modifier.padding(horizontal = horizontalPadding),
|
||||
config = banner.config,
|
||||
containerColor = containerColor,
|
||||
)
|
||||
|
|
@ -253,7 +263,11 @@ private fun previewState(bannerCount: Int) = PromoBannersBlockUM(
|
|||
@Composable
|
||||
private fun Preview_PromoBannersBlock_Legacy() {
|
||||
TangemThemePreview {
|
||||
PromoBannersBlock(state = previewState(bannerCount = 2), modifier = Modifier.padding(16.dp))
|
||||
PromoBannersBlock(
|
||||
state = previewState(bannerCount = 2),
|
||||
horizontalItemPadding = 12.dp,
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -262,7 +276,11 @@ private fun Preview_PromoBannersBlock_Legacy() {
|
|||
@Composable
|
||||
private fun Preview_PromoBannersBlock_Redesign() {
|
||||
TangemThemePreviewRedesign {
|
||||
PromoBannersBlock(state = previewState(bannerCount = 2), modifier = Modifier.padding(16.dp))
|
||||
PromoBannersBlock(
|
||||
state = previewState(bannerCount = 2),
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
horizontalItemPadding = 12.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<Boolean, BigDecimal> {
|
||||
): 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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,164 @@
|
|||
package com.tangem.features.tangempay.entity
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.pay.TangemPayCard
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.domain.models.pay.TangemPayCardState
|
||||
import com.tangem.features.tangempay.utils.TangemPayDetailIntents
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
|
||||
internal class TangemPayDetailsStateFactoryTest {
|
||||
|
||||
private val intents: TangemPayDetailIntents = mockk(relaxed = true)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(intents)
|
||||
}
|
||||
|
||||
private val activeUnfrozenCard = TangemPayCard(
|
||||
id = "card_1",
|
||||
productInstanceId = "pi_card_1",
|
||||
cardStatus = TangemPayCard.Status.ACTIVE,
|
||||
hasPinCode = false,
|
||||
displayName = null,
|
||||
frozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
lastDigits = "1234",
|
||||
limit = null,
|
||||
state = TangemPayCardState.Active,
|
||||
)
|
||||
|
||||
private fun createFactory() = TangemPayDetailsStateFactory(
|
||||
onBack = {},
|
||||
onOpenMenu = {},
|
||||
intents = intents,
|
||||
isRedesignEnabled = true,
|
||||
isRemoveAccountEnabled = true,
|
||||
isMultipleCardsEnabled = true,
|
||||
)
|
||||
|
||||
private fun loadedStatus(
|
||||
statusSource: StatusSource,
|
||||
statusError: PaymentAccountStatusValue.Error?,
|
||||
statusCards: List<TangemPayCard> = listOf(activeUnfrozenCard),
|
||||
): PaymentAccountStatusValue.Loaded = mockk(relaxed = true) {
|
||||
every { source } returns statusSource
|
||||
every { error } returns statusError
|
||||
every { cards } returns statusCards
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideButtonStateCases")
|
||||
fun `GIVEN status source WHEN getLoadedState THEN action buttons gated by freshness but card tile only by error`(
|
||||
case: ButtonStateCase,
|
||||
) {
|
||||
// Arrange
|
||||
val factory = createFactory()
|
||||
val status = loadedStatus(statusSource = case.source, statusError = case.error)
|
||||
|
||||
// Act
|
||||
val state = factory.getLoadedState(status)
|
||||
|
||||
// Assert
|
||||
val actionButtonsEnabled = state.balanceBlockState.actionButtons.map { it.isEnabled }
|
||||
assertThat(actionButtonsEnabled).containsExactly(case.expectedEnabled, case.expectedEnabled)
|
||||
assertThat(state.balanceBlockState.cardsBlockState?.isAddCardEnabled).isEqualTo(case.expectedEnabled)
|
||||
// The card tile is intentionally NOT source-gated: it stays clickable on stale data as long as
|
||||
// there is no error, so the user can still view the card details offline (which self-gate actions).
|
||||
assertThat(state.balanceBlockState.cardsBlockState?.cards?.single()?.isEnabled)
|
||||
.isEqualTo(case.expectedCardEnabled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN actual status with only frozen card WHEN getLoadedState THEN action buttons disabled`() {
|
||||
// Arrange
|
||||
val factory = createFactory()
|
||||
val frozenCard = activeUnfrozenCard.copy(frozenState = TangemPayCardFrozenState.Frozen)
|
||||
val status = loadedStatus(
|
||||
statusSource = StatusSource.ACTUAL,
|
||||
statusError = null,
|
||||
statusCards = listOf(frozenCard),
|
||||
)
|
||||
|
||||
// Act
|
||||
val state = factory.getLoadedState(status)
|
||||
|
||||
// Assert
|
||||
assertThat(state.balanceBlockState.actionButtons.map { it.isEnabled }).containsExactly(false, false)
|
||||
assertThat(state.balanceBlockState.cardsBlockState?.isAddCardEnabled).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN actual status with issuing card WHEN getLoadedState THEN add card disabled`() {
|
||||
// Arrange
|
||||
val factory = createFactory()
|
||||
val issuingCard = activeUnfrozenCard.copy(state = TangemPayCardState.Issuing)
|
||||
val status = loadedStatus(
|
||||
statusSource = StatusSource.ACTUAL,
|
||||
statusError = null,
|
||||
statusCards = listOf(activeUnfrozenCard, issuingCard),
|
||||
)
|
||||
|
||||
// Act
|
||||
val state = factory.getLoadedState(status)
|
||||
|
||||
// Assert
|
||||
assertThat(state.balanceBlockState.cardsBlockState?.isAddCardEnabled).isFalse()
|
||||
}
|
||||
|
||||
internal data class ButtonStateCase(
|
||||
val source: StatusSource,
|
||||
val error: PaymentAccountStatusValue.Error?,
|
||||
val expectedEnabled: Boolean,
|
||||
val expectedCardEnabled: Boolean,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
@JvmStatic
|
||||
fun provideButtonStateCases() = listOf(
|
||||
// Fresh data from the network -> actions allowed, card tile clickable.
|
||||
ButtonStateCase(
|
||||
source = StatusSource.ACTUAL,
|
||||
error = null,
|
||||
expectedEnabled = true,
|
||||
expectedCardEnabled = true,
|
||||
),
|
||||
// Cache restored from disk before a refresh confirms it -> actions blocked, card tile clickable.
|
||||
ButtonStateCase(
|
||||
source = StatusSource.CACHE,
|
||||
error = null,
|
||||
expectedEnabled = false,
|
||||
expectedCardEnabled = true,
|
||||
),
|
||||
// Internet unavailable, only cache left ([REDACTED_TASK_KEY] case 1) -> actions blocked, card tile clickable.
|
||||
ButtonStateCase(
|
||||
source = StatusSource.ONLY_CACHE,
|
||||
error = null,
|
||||
expectedEnabled = false,
|
||||
expectedCardEnabled = true,
|
||||
),
|
||||
// Expired refresh token, only cache left ([REDACTED_TASK_KEY] case 2) -> everything blocked by the error.
|
||||
ButtonStateCase(
|
||||
source = StatusSource.ONLY_CACHE,
|
||||
error = PaymentAccountStatusValue.Error.NotSynced,
|
||||
expectedEnabled = false,
|
||||
expectedCardEnabled = false,
|
||||
),
|
||||
// Transient error overlaid on actual data -> everything blocked by the error.
|
||||
ButtonStateCase(
|
||||
source = StatusSource.ACTUAL,
|
||||
error = PaymentAccountStatusValue.Error.Unavailable,
|
||||
expectedEnabled = false,
|
||||
expectedCardEnabled = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<TokenDetailsUM> {
|
||||
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ^
|
||||
|
|
|
|||
|
|
@ -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\$"))
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue