Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-18 18:13:29 +03:00
parent 6f047b8607
commit 7fe2a4d823
6 changed files with 89 additions and 28 deletions

View file

@ -2,6 +2,7 @@ package com.tangem.core.ui.components.fields
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -36,9 +37,9 @@ fun PinTextField(
value: String,
length: Int,
isPasswordVisual: Boolean,
pinTextColor: PinTextColor,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
wrongCode: Boolean = false,
) {
val focusRequester = remember { FocusRequester() }
val textFieldValue = remember(value) {
@ -72,7 +73,7 @@ fun PinTextField(
CellDecoration(
length = length,
isPasswordVisual = isPasswordVisual,
wrongCode = wrongCode,
pinTextColor = pinTextColor,
value = value,
)
},
@ -84,17 +85,25 @@ fun PinTextField(
}
}
@Suppress("MagicNumber")
enum class PinTextColor {
Primary,
WrongCode,
Success,
}
@Suppress("MagicNumber", "LongMethod")
@Composable
private fun CellDecoration(
length: Int,
wrongCode: Boolean,
pinTextColor: PinTextColor,
value: String,
modifier: Modifier = Modifier,
isPasswordVisual: Boolean = false,
) {
val textMeasurer = rememberTextMeasurer()
val width = textMeasurer.measure("0")
val minSize = textMeasurer.measure("0")
val minWidth = maxOf(minSize.size.width.dp + 8.dp, 24.dp + 3.dp) // 24.dp is the minimum width of a pin cell
val minHeight = maxOf(minSize.size.height.dp, 48.dp) // 48.dp is the minimum height of a pin cell
Row(
modifier = modifier,
@ -107,6 +116,18 @@ private fun CellDecoration(
""
}
val color = when (pinTextColor) {
PinTextColor.Primary -> {
if (isPasswordVisual) {
TangemTheme.colors.icon.informative
} else {
TangemTheme.colors.text.primary1
}
}
PinTextColor.WrongCode -> TangemTheme.colors.icon.warning
PinTextColor.Success -> TangemTheme.colors.icon.accent
}
Box(
modifier = Modifier
.background(
@ -119,26 +140,34 @@ private fun CellDecoration(
targetState = char,
transitionSpec = {
(
fadeIn(animationSpec = tween(220, delayMillis = 90)) +
slideInVertically(animationSpec = tween(330, delayMillis = 0))
fadeIn(animationSpec = tween(90, delayMillis = 90)) +
slideInVertically(animationSpec = tween(220, delayMillis = 0))
)
.togetherWith(
fadeOut(animationSpec = tween(90)) + slideOutVertically(tween(220)),
)
},
) { text ->
Text(
modifier = Modifier.sizeIn(minWidth = width.size.width.dp + 8.dp, minHeight = 48.dp),
text = text,
style = TangemTheme.typography.h3,
color = if (wrongCode) {
TangemTheme.colors.text.warning
} else {
TangemTheme.colors.text.primary1
},
textAlign = TextAlign.Center,
lineHeight = 48.sp,
)
if (isPasswordVisual && text.isNotEmpty()) {
Canvas(
Modifier.sizeIn(minWidth = minWidth, minHeight = minHeight),
) {
drawCircle(
color = color,
radius = 4.dp.toPx(),
center = center,
)
}
} else {
Text(
modifier = Modifier.sizeIn(minWidth = minWidth, minHeight = minHeight),
text = text,
style = TangemTheme.typography.h3,
color = color,
textAlign = TextAlign.Center,
lineHeight = 48.sp,
)
}
}
}
}
@ -152,10 +181,18 @@ private fun Preview() {
var text by remember { mutableStateOf("123") }
Column {
PinTextField(
value = text,
onValueChange = { text = it },
isPasswordVisual = true,
pinTextColor = PinTextColor.Success,
length = 6,
)
PinTextField(
value = text,
onValueChange = { text = it },
isPasswordVisual = false,
pinTextColor = PinTextColor.Primary,
length = 6,
)

View file

@ -12,6 +12,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.res.R
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.fields.PinTextColor
import com.tangem.core.ui.components.fields.PinTextField
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
@ -75,6 +76,7 @@ internal fun AccessCode(state: AccessCodeUM, modifier: Modifier = Modifier) {
length = state.accessCodeLength,
isPasswordVisual = true,
value = state.accessCode,
pinTextColor = PinTextColor.Primary,
onValueChange = state.onAccessCodeChange,
)
}

View file

@ -26,8 +26,7 @@ internal class DefaultHotAccessCodeRequestComponent @AssistedInject constructor(
}
override suspend fun successfulAuthentication() {
// TODO handle successful authentication
// TODO add delay
model.successfulAuthentication()
}
override suspend fun requestPassword(hasBiometry: Boolean): HotWalletPasswordRequester.Result {

View file

@ -2,6 +2,7 @@ package com.tangem.features.hotwallet.accesscoderequest
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.ui.components.fields.PinTextColor
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH
import com.tangem.features.hotwallet.accesscoderequest.entity.HotAccessCodeRequestUM
@ -45,13 +46,23 @@ internal class HotAccessCodeRequestModel @Inject constructor(
suspend fun wrongAccessCode() {
uiState.update {
it.copy(
wrongAccessCode = true,
accessCodeColor = PinTextColor.WrongCode,
onAccessCodeChange = {},
)
}
delay(timeMillis = 500) // Delay to show the wrong access code state
}
suspend fun successfulAuthentication() {
uiState.update {
it.copy(
accessCodeColor = PinTextColor.Success,
onAccessCodeChange = {},
)
}
delay(timeMillis = 200) // Delay to show the success state
}
private fun getInitialState() = HotAccessCodeRequestUM(
onDismiss = ::dismiss,
onAccessCodeChange = ::onAccessCodeChange,
@ -66,7 +77,10 @@ internal class HotAccessCodeRequestModel @Inject constructor(
if (accessCode.length > ACCESS_CODE_LENGTH) return
uiState.update {
it.copy(accessCode = accessCode, wrongAccessCode = false)
it.copy(
accessCode = accessCode,
accessCodeColor = PinTextColor.Primary,
)
}
if (accessCode.length == ACCESS_CODE_LENGTH) {

View file

@ -1,9 +1,11 @@
package com.tangem.features.hotwallet.accesscoderequest.entity
import com.tangem.core.ui.components.fields.PinTextColor
internal data class HotAccessCodeRequestUM(
val isShown: Boolean = false,
val accessCode: String = "",
val wrongAccessCode: Boolean = false,
val accessCodeColor: PinTextColor = PinTextColor.Primary,
val useBiometricVisible: Boolean = true,
val useBiometricClick: () -> Unit = {},
val onAccessCodeChange: (String) -> Unit = {},

View file

@ -20,6 +20,7 @@ import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.fields.PinTextColor
import com.tangem.core.ui.components.fields.PinTextField
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.haptic.TangemHapticEffect
@ -85,7 +86,7 @@ internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM
length = 6,
isPasswordVisual = true,
value = state.accessCode,
wrongCode = state.wrongAccessCode,
pinTextColor = state.accessCodeColor,
onValueChange = state.onAccessCodeChange,
)
}
@ -106,9 +107,15 @@ internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM
val hapticManager = LocalHapticManager.current
LaunchedEffect(state.wrongAccessCode) {
if (state.wrongAccessCode) {
hapticManager.perform(TangemHapticEffect.View.Reject)
LaunchedEffect(state.accessCodeColor) {
when (state.accessCodeColor) {
PinTextColor.WrongCode -> {
hapticManager.perform(TangemHapticEffect.View.Reject)
}
PinTextColor.Success -> {
hapticManager.perform(TangemHapticEffect.View.Confirm)
}
else -> Unit
}
}
}