Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-09 09:35:15 +02:00
parent db9ec70213
commit 29cbad3bb5
8 changed files with 256 additions and 91 deletions

View file

@ -52,6 +52,7 @@ internal class TangemPayEditDisplayNameComponent(
cardDetailsBlockComponent = cardDetailsBlockComponent,
cardDetailsState = editingCardDetailsState,
modifier = modifier,
isRedesignEnabled = model.isRedesignEnabled(),
)
}
}

View file

@ -18,6 +18,7 @@ import com.tangem.domain.models.account.requireCardWithId
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.pay.usecase.UpdateTangemPayCardNameUseCase
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayEditDisplayNameUM
@ -28,6 +29,7 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@Stable
@ModelScoped
internal class TangemPayEditDisplayNameModel @Inject constructor(
@ -37,6 +39,7 @@ internal class TangemPayEditDisplayNameModel @Inject constructor(
private val updateCardNameUseCase: UpdateTangemPayCardNameUseCase,
private val uiMessageSender: UiMessageSender,
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
private val featureToggles: TangemPayFeatureToggles,
) : Model() {
private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require()
@ -63,6 +66,8 @@ internal class TangemPayEditDisplayNameModel @Inject constructor(
subscribeToCardNameChanges(card.id, params.initialStatus.userWalletId)
}
fun isRedesignEnabled() = featureToggles.isRedesignEnabled
private fun subscribeToCardNameChanges(cardId: String, userWalletId: UserWalletId) {
paymentAccountStatusSupplier.invoke(userWalletId)
.onEach { state ->

View file

@ -38,6 +38,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.constraintlayout.compose.ConstrainedLayoutReference
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.ConstraintLayoutScope
@ -49,6 +50,7 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.*
@ -62,6 +64,7 @@ import com.tangem.features.tangempay.model.CardDataType
private const val TEXT_WIDTH_PADDING = 2
private const val FREEZE_ANIMATION_DURATION_MS = 600
private val CustomCardBlockColor = Color(0x1F828282)
private val CardBackgroundColor = Color(0xFF171A27)
@Suppress("MagicNumber")
@Composable
@ -80,26 +83,10 @@ internal fun TangemPayCard(state: TangemPayCardDetailsUM, modifier: Modifier = M
// Determine which side to show based on the rotation angle
val shouldShowDetails = rotateCardY > 90f
Box(
modifier = modifier
.fillMaxWidth()
.aspectRatio(328f / 212f) // size of img_tangem_pay_visa
.graphicsLayer {
rotationY = rotateCardY
cameraDistance = zAxisDistance
}
.clip(RoundedCornerShape(16.dp))
.background(Color(red = 18, green = 21, blue = 31))
.border(
width = 1.dp,
brush = Brush.linearGradient(
colors = listOf(
TangemTheme.colors.text.constantWhite.copy(alpha = 0.1F),
TangemTheme.colors.text.constantWhite.copy(alpha = 0f),
),
),
shape = RoundedCornerShape(16.dp),
),
CardBgWrapper(
rotateCardY = rotateCardY,
zAxisDistance = zAxisDistance,
modifier = modifier,
) {
if (shouldShowDetails) {
TangemPayCardDetailsShownBlock(
@ -113,9 +100,7 @@ internal fun TangemPayCard(state: TangemPayCardDetailsUM, modifier: Modifier = M
modifier = Modifier.graphicsLayer { rotationY = 180f },
)
} else {
TangemPayCardDetailsHiddenBlock(
state = state,
)
TangemPayCardDetailsHiddenBlock(state = state)
}
}
}
@ -123,79 +108,94 @@ internal fun TangemPayCard(state: TangemPayCardDetailsUM, modifier: Modifier = M
@Suppress("LongMethod", "DestructuringDeclarationWithTooManyEntries")
@Composable
private fun TangemPayCardDetailsHiddenBlock(state: TangemPayCardDetailsUM, modifier: Modifier = Modifier) {
val isRenaming = state.displayNameState is DisplayNameState.Editing
Box(modifier = modifier.fillMaxSize()) {
TangemPayCardBackground(cardFrozenState = state.cardFrozenState)
CardTopBlock()
TangemPayCardBackground(
modifier = Modifier
.fillMaxSize()
.zIndex(0f),
isRenaming = isRenaming,
cardFrozenState = state.cardFrozenState,
)
if (state.isActionsAvailable) {
ConstraintLayout(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(horizontal = 16.dp)
.padding(bottom = 8.dp)
.fillMaxWidth(),
) {
val (displayNameRef, cardNumberRef, frozenIconRef, buttonRef) = createRefs()
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(1f),
) {
CardTopBlock()
if (state.displayNameState != null) {
CardDisplayName(
state = state.displayNameState,
modifier = Modifier.constrainAs(displayNameRef) {
start.linkTo(parent.start)
bottom.linkTo(cardNumberRef.top)
width = Dimension.wrapContent
},
)
}
CardNumberBlock(
numberShort = state.numberShort,
cardNumberRef = cardNumberRef,
)
when (state.cardFrozenState) {
TangemPayCardFrozenState.Frozen -> Icon(
modifier = Modifier
.constrainAs(frozenIconRef) {
start.linkTo(cardNumberRef.end, margin = 4.dp)
top.linkTo(cardNumberRef.top)
bottom.linkTo(cardNumberRef.bottom)
}
.padding(bottom = 8.dp)
.size(16.dp)
.testTag(TangemPayTestTags.CARD_FROZEN_BADGE),
painter = painterResource(id = R.drawable.ic_snow_24),
contentDescription = null,
tint = TangemTheme.colors.icon.constant,
)
TangemPayCardFrozenState.Pending -> CircularProgressIndicator(
modifier = Modifier
.constrainAs(frozenIconRef) {
start.linkTo(cardNumberRef.end, margin = 4.dp)
top.linkTo(cardNumberRef.top)
bottom.linkTo(cardNumberRef.bottom)
}
.padding(bottom = 8.dp)
.size(16.dp)
.testTag(TangemPayTestTags.CARD_FROZEN_BADGE),
color = TangemTheme.colors.text.constantWhite,
strokeWidth = 1.dp,
)
TangemPayCardFrozenState.Unfrozen -> Unit
}
AnimatedVisibility(
if (state.isActionsAvailable) {
ConstraintLayout(
modifier = Modifier
.constrainAs(buttonRef) {
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
}
.testTag(TangemPayTestTags.CARD_DETAILS_SHOW_BUTTON),
visible = !LocalVisaRedesignEnabled.current || state.isLoading,
.align(Alignment.BottomCenter)
.padding(horizontal = 16.dp)
.padding(bottom = 8.dp)
.fillMaxWidth(),
) {
TangemPayCardDetailsCustomButton(
text = stringResourceSafe(id = R.string.tangempay_card_details_show_details),
onClick = state.onClick,
showProgress = state.isLoading,
val (displayNameRef, cardNumberRef, frozenIconRef, buttonRef) = createRefs()
if (state.displayNameState != null) {
CardDisplayName(
state = state.displayNameState,
modifier = Modifier.constrainAs(displayNameRef) {
start.linkTo(parent.start)
bottom.linkTo(cardNumberRef.top)
width = Dimension.wrapContent
},
)
}
CardNumberBlock(
numberShort = state.numberShort,
cardNumberRef = cardNumberRef,
)
when (state.cardFrozenState) {
TangemPayCardFrozenState.Frozen -> Icon(
modifier = Modifier
.constrainAs(frozenIconRef) {
start.linkTo(cardNumberRef.end, margin = 4.dp)
top.linkTo(cardNumberRef.top)
bottom.linkTo(cardNumberRef.bottom)
}
.padding(bottom = 8.dp)
.size(16.dp)
.testTag(TangemPayTestTags.CARD_FROZEN_BADGE),
painter = painterResource(id = R.drawable.ic_snow_24),
contentDescription = null,
tint = TangemTheme.colors.icon.constant,
)
TangemPayCardFrozenState.Pending -> CircularProgressIndicator(
modifier = Modifier
.constrainAs(frozenIconRef) {
start.linkTo(cardNumberRef.end, margin = 4.dp)
top.linkTo(cardNumberRef.top)
bottom.linkTo(cardNumberRef.bottom)
}
.padding(bottom = 8.dp)
.size(16.dp)
.testTag(TangemPayTestTags.CARD_FROZEN_BADGE),
color = TangemTheme.colors.text.constantWhite,
strokeWidth = 1.dp,
)
TangemPayCardFrozenState.Unfrozen -> Unit
}
AnimatedVisibility(
modifier = Modifier
.constrainAs(buttonRef) {
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
}
.testTag(TangemPayTestTags.CARD_DETAILS_SHOW_BUTTON),
visible = !LocalVisaRedesignEnabled.current || state.isLoading,
) {
TangemPayCardDetailsCustomButton(
text = stringResourceSafe(id = R.string.tangempay_card_details_show_details),
onClick = state.onClick,
showProgress = state.isLoading,
)
}
}
}
}
@ -203,7 +203,11 @@ private fun TangemPayCardDetailsHiddenBlock(state: TangemPayCardDetailsUM, modif
}
@Composable
private fun TangemPayCardBackground(cardFrozenState: TangemPayCardFrozenState, modifier: Modifier = Modifier) {
private fun TangemPayCardBackground(
isRenaming: Boolean,
cardFrozenState: TangemPayCardFrozenState,
modifier: Modifier = Modifier,
) {
val isFrozen = cardFrozenState == TangemPayCardFrozenState.Frozen
val freezeProgress by animateFloatAsState(
targetValue = if (isFrozen) 1f else 0f,
@ -221,6 +225,14 @@ private fun TangemPayCardBackground(cardFrozenState: TangemPayCardFrozenState, m
contentDescription = null,
)
if (isRenaming && LocalVisaRedesignEnabled.current) {
Box(
modifier = Modifier
.fillMaxSize()
.background(CardBackgroundColor.copy(alpha = 0.8f)),
)
}
if (isFrozen || freezeProgress > 0f) {
Image(
modifier = Modifier
@ -233,6 +245,69 @@ private fun TangemPayCardBackground(cardFrozenState: TangemPayCardFrozenState, m
}
}
@Suppress("MagicNumber")
@Composable
private fun CardBgWrapper(
rotateCardY: Float,
zAxisDistance: Float,
modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit,
) {
val isRedesignEnabled = LocalVisaRedesignEnabled.current
val shouldShowDetailsBg = rotateCardY > 90f && isRedesignEnabled
Box(
modifier = modifier
.fillMaxWidth()
.aspectRatio(328f / 212f) // size of img_tangem_pay_visa
.graphicsLayer {
rotationY = rotateCardY
cameraDistance = zAxisDistance
}
.conditionalCompose(
condition = isRedesignEnabled,
modifier = {
clip(RoundedCornerShape(TangemTheme.dimens2.x5))
.border(
width = 1.dp,
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
color = TangemTheme.colors3.border.secondary,
)
.background(CardBackgroundColor)
},
otherModifier = {
clip(RoundedCornerShape(16.dp))
.background(CardBackgroundColor)
.border(
width = 1.dp,
brush = Brush.linearGradient(
colors = listOf(
TangemTheme.colors.text.constantWhite.copy(alpha = 0.1F),
TangemTheme.colors.text.constantWhite.copy(alpha = 0f),
),
),
shape = RoundedCornerShape(16.dp),
)
},
),
) {
Box(modifier = Modifier.fillMaxWidth()) {
if (shouldShowDetailsBg) {
Image(
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
rotationY = rotateCardY
cameraDistance = zAxisDistance
},
painter = painterResource(R.drawable.img_bg_card_details),
contentDescription = null,
)
}
content()
}
}
}
@Composable
private fun CardTopBlock(modifier: Modifier = Modifier) {
Row(

View file

@ -2,6 +2,8 @@ package com.tangem.features.tangempay.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
@ -9,8 +11,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
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_cross_20
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
@ -18,6 +26,31 @@ import com.tangem.features.tangempay.entity.TangemPayEditDisplayNameUM
@Composable
internal fun TangemPayEditDisplayNameScreen(
isRedesignEnabled: Boolean,
state: TangemPayEditDisplayNameUM,
cardDetailsBlockComponent: TangemPayCardDetailsBlockComponent,
cardDetailsState: TangemPayCardDetailsUM,
modifier: Modifier = Modifier,
) {
if (isRedesignEnabled) {
TangemPayEditDisplayNameScreenV2(
state = state,
cardDetailsBlockComponent = cardDetailsBlockComponent,
cardDetailsState = cardDetailsState,
modifier = modifier,
)
} else {
TangemPayEditDisplayNameScreenV1(
state = state,
cardDetailsBlockComponent = cardDetailsBlockComponent,
cardDetailsState = cardDetailsState,
modifier = modifier,
)
}
}
@Composable
internal fun TangemPayEditDisplayNameScreenV1(
state: TangemPayEditDisplayNameUM,
cardDetailsBlockComponent: TangemPayCardDetailsBlockComponent,
cardDetailsState: TangemPayCardDetailsUM,
@ -67,4 +100,55 @@ internal fun TangemPayEditDisplayNameScreen(
enabled = !state.isLoading && state.isDoneEnabled,
)
}
}
@Composable
internal fun TangemPayEditDisplayNameScreenV2(
state: TangemPayEditDisplayNameUM,
cardDetailsBlockComponent: TangemPayCardDetailsBlockComponent,
cardDetailsState: TangemPayCardDetailsUM,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxSize()
.background(TangemTheme.colors3.bg.primary)
.systemBarsPadding(),
) {
TangemTopBar(
endContent = {
TangemButton(
iconStart = TangemIconUM.Icon(imageVector = Icons.ic_cross_20),
onClick = state.onDismiss,
size = TangemButton.Size.X11,
variant = TangemButton.Variant.Material,
)
},
title = resourceReference(R.string.tangem_pay_rename_card_title),
)
Column(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
) {
cardDetailsBlockComponent.CardDetailsBlockContent(
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x4, vertical = TangemTheme.dimens2.x2),
state = cardDetailsState,
)
}
TangemButton(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = TangemTheme.dimens2.x3, horizontal = TangemTheme.dimens2.x4)
.imePadding(),
text = resourceReference(R.string.common_save),
onClick = state.onDoneClick,
isLoading = state.isLoading,
isEnabled = !state.isLoading && state.isDoneEnabled,
size = TangemButton.Size.X12,
)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB