Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-10 17:43:38 +02:00
parent 740da6cddd
commit c443fa43c3
9 changed files with 369 additions and 91 deletions

View file

@ -94,6 +94,7 @@ internal class TangemPayCardPageComponent @AssistedInject constructor(
)
TangemPayCardDetailsInnerRoute.LimitSetupSuccess -> TangemPayCardLimitSetupSuccessComponent(
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),
isRedesignEnabled = tangemPayFeatureToggles.isRedesignEnabled,
)
}

View file

@ -21,9 +21,16 @@ internal class TangemPayCardLimitSetupComponent(
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
BackHandler(onBack = router::pop)
TangemPayCardLimitSetupScreen(
state = state,
modifier = modifier,
)
if (model.inRedesignEnabled()) {
TangemPayCardLimitSetupScreenV2(
state = state,
modifier = modifier,
)
} else {
TangemPayCardLimitSetupScreen(
state = state,
modifier = modifier,
)
}
}
}

View file

@ -22,6 +22,7 @@ import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
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.navigation.TangemPayCardDetailsInnerRoute
@ -47,6 +48,7 @@ internal class TangemPayCardLimitSetupModel @Inject constructor(
private val setTangemPayCardLimitUseCase: SetTangemPayCardLimitUseCase,
private val uiMessageSender: UiMessageSender,
private val analytics: AnalyticsEventHandler,
private val featureToggles: TangemPayFeatureToggles,
) : Model() {
private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require()
@ -78,6 +80,8 @@ internal class TangemPayCardLimitSetupModel @Inject constructor(
observeCardState()
}
fun inRedesignEnabled(): Boolean = featureToggles.isRedesignEnabled
private fun observeCardState() {
paymentAccountStatusSupplier.invoke(userWalletId)
.map { it.value }

View file

@ -0,0 +1,186 @@
package com.tangem.features.tangempay.limit.setup
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.fields.AmountTextField
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
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.ds2.shimmers.TextShimmer
import com.tangem.core.ui.ds2.shimmers.TextShimmerStyle
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.utils.rememberDecimalFormat
import com.tangem.features.tangempay.details.impl.R
import kotlinx.collections.immutable.ImmutableList
@Composable
internal fun TangemPayCardLimitSetupScreenV2(state: TangemPayCardLimitSetupUM, modifier: Modifier = Modifier) {
Scaffold(
modifier = modifier,
topBar = {
TangemTopBar(
modifier = Modifier.statusBarsPadding(),
startContent = {
TangemButton(
iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_arrow_back_28),
onClick = state.onBackClick,
size = TangemButton.Size.X11,
variant = TangemButton.Variant.Material,
)
},
title = resourceReference(R.string.tangempay_card_page_daily_limit_title),
)
},
containerColor = TangemTheme.colors3.bg.secondary,
) { scaffoldPaddings ->
Content(
state = state,
modifier = Modifier.padding(scaffoldPaddings),
)
}
}
@Composable
private fun Content(state: TangemPayCardLimitSetupUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.padding(top = TangemTheme.dimens2.x4, bottom = TangemTheme.dimens2.x3)
.imePadding(),
) {
AmountBlock(
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x4),
state = state,
)
Spacer(modifier = Modifier.weight(1f))
PresetsRow(presets = state.presets)
TangemButton(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens2.x4)
.padding(top = TangemTheme.dimens2.x3),
size = TangemButton.Size.X12,
text = resourceReference(R.string.tangempay_daily_limit_set_button),
onClick = state.onSubmitClick,
isEnabled = state.isSubmitButtonEnabled,
isLoading = state.isSubmitButtonLoading,
)
}
}
@Composable
private fun AmountBlock(state: TangemPayCardLimitSetupUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(vertical = TangemTheme.dimens2.x8),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x3),
) {
Text(
text = state.subtitle.resolveReference(),
style = TangemTheme.typography3.subheading.medium,
color = TangemTheme.colors3.text.tertiary,
)
if (state.isInitialDataLoading) {
TextShimmer(
style = TextShimmerStyle.HEADING_MEDIUM,
text = "$ 10000",
radius = TangemTheme.dimens2.x25,
)
} else {
AmountTextField(
value = state.amountFieldModel.value,
decimals = state.amountFieldModel.decimals,
onValueChange = state.amountFieldModel.onValueChange,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
visualTransformation = AmountVisualTransformation(
decimals = state.amountFieldModel.decimals,
symbol = state.currencyCode,
currencyCode = state.currencyCode,
decimalFormat = rememberDecimalFormat(),
symbolColor = if (state.amountFieldModel.value.isBlank()) {
TangemTheme.colors3.text.tertiary
} else {
TangemTheme.colors3.text.primary
},
),
textStyle = TangemTheme.typography3.display.medium.copy(
textAlign = TextAlign.Center,
),
isAutoResize = true,
backgroundColor = TangemTheme.colors3.bg.secondary,
)
}
}
}
@Composable
private fun PresetsRow(presets: ImmutableList<TangemPayCardLimitSetupUM.LimitPresetUM>) {
if (presets.isEmpty()) return
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens2.x3, vertical = 6.dp),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
) {
presets.forEach { preset ->
PresetChip(
preset = preset,
modifier = Modifier.weight(1f),
)
}
}
}
@Composable
private fun PresetChip(preset: TangemPayCardLimitSetupUM.LimitPresetUM, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.clip(RoundedCornerShape(14.dp))
.background(TangemTheme.colors3.bg.tertiary)
.clickable(onClick = preset.onClick)
.padding(horizontal = TangemTheme.dimens2.x5, vertical = TangemTheme.dimens2.x1)
.wrapContentHeight(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier
.padding(vertical = 1.dp)
.fillMaxWidth(),
text = preset.label,
style = TangemTheme.typography3.subheading.medium,
color = TangemTheme.colors3.text.primary,
textAlign = TextAlign.Center,
maxLines = 1,
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun preview() = TangemThemePreviewRedesign {
TangemPayCardLimitSetupScreenV2(
state = TangemPayCardLimitSetupUM.stub(),
)
}

View file

@ -8,16 +8,24 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.tangempay.navigation.TangemPayCardDetailsInnerRoute
internal class TangemPayCardLimitSetupSuccessComponent(
private val isRedesignEnabled: Boolean,
appComponentContext: AppComponentContext,
) : AppComponentContext by appComponentContext, ComposableContentComponent {
@Composable
override fun Content(modifier: Modifier) {
BackHandler(onBack = ::backToDetails)
TangemPayCardLimitSetupSuccessScreen(
modifier = modifier,
onDoneClick = ::backToDetails,
)
if (isRedesignEnabled) {
TangemPayCardLimitSetupSuccessScreenV2(
modifier = modifier,
onDoneClick = ::backToDetails,
)
} else {
TangemPayCardLimitSetupSuccessScreen(
modifier = modifier,
onDoneClick = ::backToDetails,
)
}
}
private fun backToDetails() {

View file

@ -0,0 +1,28 @@
package com.tangem.features.tangempay.limit.setup
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.ui.components.TangemPaySuccessScreenWrapper
@Composable
internal fun TangemPayCardLimitSetupSuccessScreenV2(onDoneClick: () -> Unit, modifier: Modifier = Modifier) {
TangemPaySuccessScreenWrapper(
modifier = modifier,
title = resourceReference(R.string.tangempay_card_page_daily_limit_success_title),
subtitle = resourceReference(R.string.tangempay_card_page_daily_limit_success_description),
buttonText = resourceReference(R.string.common_done),
onButtonClick = onDoneClick,
)
}
@Preview(showBackground = true)
@Composable
private fun Preview() {
TangemThemePreviewRedesign {
TangemPayCardLimitSetupSuccessScreenV2(onDoneClick = {})
}
}

View file

@ -1,92 +1,25 @@
package com.tangem.features.tangempay.ui
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerHMax
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.TangemThemePreviewRedesign
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_success_24
import com.tangem.core.ui.test.TangemPayTestTags
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.ui.components.TangemPaySuccessScreenWrapper
private const val BG_GREEN_COLOR = 0xFF9FC824
@Suppress("MagicNumber")
@Composable
internal fun TangemPayChangePinCodeSuccessScreenV2(onClose: () -> Unit, modifier: Modifier = Modifier) {
Box(modifier = modifier.fillMaxSize()) {
Box(
modifier = Modifier
.matchParentSize()
.blur(192.dp)
.drawBehind {
val w = size.width
drawRect(
brush = Brush.radialGradient(
colors = listOf(
Color(BG_GREEN_COLOR),
Color.Transparent,
),
center = Offset(w / 2f, -w * .1f),
radius = w + w * .2f,
tileMode = TileMode.Clamp,
),
)
},
)
Column(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.systemBars)
.padding(top = 72.dp, start = 24.dp, end = 24.dp),
) {
Icon(
modifier = Modifier.size(28.dp),
imageVector = Icons.ic_success_24,
tint = TangemTheme.colors3.icon.primary,
contentDescription = null,
)
SpacerH(TangemTheme.dimens2.x4)
Text(
modifier = Modifier.testTag(TangemPayTestTags.PIN_SUCCESS_TITLE),
text = stringResourceSafe(R.string.tangempay_card_details_change_pin_success_title),
style = TangemTheme.typography3.heading.medium,
color = TangemTheme.colors3.text.primary,
)
Text(
modifier = Modifier.testTag(TangemPayTestTags.PIN_SUCCESS_DESCRIPTION),
text = stringResourceSafe(R.string.tangempay_card_details_change_pin_success_description),
style = TangemTheme.typography3.heading.medium,
color = TangemTheme.colors3.text.secondary,
)
SpacerHMax()
TangemButton(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = TangemTheme.dimens2.x3),
onClick = onClose,
size = TangemButton.Size.X12,
text = resourceReference(R.string.common_close),
)
}
}
TangemPaySuccessScreenWrapper(
modifier = modifier,
title = resourceReference(R.string.tangempay_card_details_change_pin_success_title),
subtitle = resourceReference(R.string.tangempay_card_details_change_pin_success_description),
buttonText = resourceReference(R.string.common_close),
onButtonClick = onClose,
titleTestTag = TangemPayTestTags.PIN_SUCCESS_TITLE,
subtitleTestTag = TangemPayTestTags.PIN_SUCCESS_DESCRIPTION,
)
}
@Preview(showBackground = true)

View file

@ -0,0 +1,113 @@
package com.tangem.features.tangempay.ui.components
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerHMax
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_success_24
import com.tangem.features.tangempay.details.impl.R
private const val DEFAULT_FADE_COLOR = 0xFF9FC824
private val BlurRadius = 192.dp
@Suppress("MagicNumber")
@Composable
internal fun TangemPaySuccessScreenWrapper(
title: TextReference,
subtitle: TextReference,
buttonText: TextReference,
onButtonClick: () -> Unit,
modifier: Modifier = Modifier,
fadeColor: Color = Color(DEFAULT_FADE_COLOR),
titleTestTag: String? = null,
subtitleTestTag: String? = null,
) {
Box(modifier = modifier.fillMaxSize()) {
Box(
modifier = Modifier
.matchParentSize()
.blur(BlurRadius)
.drawBehind {
val w = size.width
drawRect(
brush = Brush.radialGradient(
colors = listOf(
fadeColor,
Color.Transparent,
),
center = Offset(w / 2f, -w * .1f),
radius = w + w * .2f,
tileMode = TileMode.Clamp,
),
)
},
)
Column(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.systemBars)
.padding(top = 72.dp, start = 24.dp, end = 24.dp),
) {
Icon(
modifier = Modifier.size(28.dp),
imageVector = Icons.ic_success_24,
tint = TangemTheme.colors3.icon.primary,
contentDescription = null,
)
SpacerH(TangemTheme.dimens2.x4)
Text(
modifier = titleTestTag?.let { Modifier.testTag(it) } ?: Modifier,
text = title.resolveReference(),
style = TangemTheme.typography3.heading.medium,
color = TangemTheme.colors3.text.primary,
)
Text(
modifier = subtitleTestTag?.let { Modifier.testTag(it) } ?: Modifier,
text = subtitle.resolveReference(),
style = TangemTheme.typography3.heading.medium,
color = TangemTheme.colors3.text.secondary,
)
SpacerHMax()
TangemButton(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = TangemTheme.dimens2.x3),
onClick = onButtonClick,
size = TangemButton.Size.X12,
text = buttonText,
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun Preview() {
TangemThemePreviewRedesign {
TangemPaySuccessScreenWrapper(
title = resourceReference(R.string.tangempay_card_details_change_pin_success_title),
subtitle = resourceReference(R.string.tangempay_card_details_change_pin_success_description),
buttonText = resourceReference(R.string.common_close),
onButtonClick = {},
)
}
}

View file

@ -9,16 +9,12 @@ import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
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.TangemPayCardLimit
import com.tangem.domain.models.pay.TangemPayCardLimitData
import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
import com.tangem.domain.models.pay.TangemPayCardState
import com.tangem.domain.models.pay.*
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.every
@ -42,6 +38,7 @@ internal class TangemPayCardLimitSetupModelTest {
private val setLimitUseCase: SetTangemPayCardLimitUseCase = mockk(relaxed = true)
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier = mockk()
private val analytics: AnalyticsEventHandler = mockk(relaxed = true)
private val featureToggles: TangemPayFeatureToggles = mockk()
private val initialCard = TangemPayCard(
id = cardId,
@ -104,6 +101,7 @@ internal class TangemPayCardLimitSetupModelTest {
setTangemPayCardLimitUseCase = setLimitUseCase,
uiMessageSender = uiMessageSender,
analytics = analytics,
featureToggles = featureToggles,
)
}