Updated on 2026-08-14
This commit is contained in:
parent
5579cc1372
commit
a04992591a
24 changed files with 707 additions and 29 deletions
|
|
@ -45,6 +45,7 @@ dependencies {
|
|||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
implementation(deps.lottie.compose)
|
||||
|
||||
/** AndroidX */
|
||||
implementation(deps.androidx.activity.compose)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.features.tangempay.components
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.stack.Children
|
||||
|
|
@ -57,18 +57,23 @@ class DefaultTangemPayDetailsContainerComponent @AssistedInject constructor(
|
|||
componentContext: ComponentContext,
|
||||
): ComposableContentComponent = when (config) {
|
||||
TangemPayDetailsInnerRoute.Details -> TangemPayDetailsComponent(
|
||||
appComponentContext = childByContext(componentContext),
|
||||
innerRouter = innerRouter,
|
||||
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),
|
||||
params = params,
|
||||
tokenReceiveComponentFactory = tokenReceiveComponentFactory,
|
||||
)
|
||||
TangemPayDetailsInnerRoute.ChangePIN -> TODO(" [REDACTED_JIRA]")
|
||||
TangemPayDetailsInnerRoute.ChangePIN -> TangemPayChangePinComponent(
|
||||
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),
|
||||
)
|
||||
TangemPayDetailsInnerRoute.ChangePINSuccess -> TangemPayChangePinSuccessComponent(
|
||||
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onChildBack() {
|
||||
when (childStack.value.active.configuration) {
|
||||
TangemPayDetailsInnerRoute.ChangePIN -> stackNavigation.pop()
|
||||
TangemPayDetailsInnerRoute.Details -> router.pop()
|
||||
if (childStack.value.backStack.isEmpty()) {
|
||||
router.pop()
|
||||
} else {
|
||||
stackNavigation.pop()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.features.tangempay.components
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect
|
||||
import com.tangem.features.tangempay.model.TangemPayChangePinModel
|
||||
import com.tangem.features.tangempay.ui.TangemPayChangePinScreen
|
||||
|
||||
internal class TangemPayChangePinComponent(
|
||||
private val appComponentContext: AppComponentContext,
|
||||
) : AppComponentContext by appComponentContext, ComposableContentComponent {
|
||||
|
||||
private val model: TangemPayChangePinModel = getOrCreateModel()
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
BackHandler(onBack = router::pop)
|
||||
DisableScreenshotsDisposableEffect()
|
||||
TangemPayChangePinScreen(
|
||||
state = state,
|
||||
onBackClick = router::pop,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.features.tangempay.components
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.tangem.features.tangempay.navigation.TangemPayDetailsInnerRoute
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.tangempay.ui.TangemPayChangePinCodeSuccessScreen
|
||||
|
||||
internal class TangemPayChangePinSuccessComponent(
|
||||
private val appComponentContext: AppComponentContext,
|
||||
) : AppComponentContext by appComponentContext, ComposableContentComponent {
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
BackHandler(onBack = ::backToDetails)
|
||||
TangemPayChangePinCodeSuccessScreen(onClick = ::backToDetails)
|
||||
}
|
||||
|
||||
private fun backToDetails() {
|
||||
router.popTo(route = TangemPayDetailsInnerRoute.Details)
|
||||
}
|
||||
}
|
||||
|
|
@ -12,25 +12,23 @@ import com.tangem.core.decompose.context.AppComponentContext
|
|||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.tangempay.components.txHistory.DefaultTangemPayTxHistoryComponent
|
||||
import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryDetailsComponent
|
||||
import com.tangem.features.tangempay.model.TangemPayDetailsModel
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation
|
||||
import com.tangem.features.tangempay.model.TangemPayDetailsModel
|
||||
import com.tangem.features.tangempay.ui.TangemPayDetailsScreen
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
|
||||
internal class TangemPayDetailsComponent(
|
||||
private val appComponentContext: AppComponentContext,
|
||||
innerRouter: Router,
|
||||
private val params: TangemPayDetailsContainerComponent.Params,
|
||||
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
|
||||
) : AppComponentContext by appComponentContext, ComposableContentComponent {
|
||||
|
||||
private val model: TangemPayDetailsModel = getOrCreateModel(params = params, router = innerRouter)
|
||||
private val model: TangemPayDetailsModel = getOrCreateModel(params = params)
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.tangempay.di
|
|||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.tangempay.model.TangemPayChangePinModel
|
||||
import com.tangem.features.tangempay.model.TangemPayDetailsModel
|
||||
import com.tangem.features.tangempay.model.TangemPayTxHistoryDetailsModel
|
||||
import com.tangem.features.tangempay.model.TangemPayTxHistoryModel
|
||||
|
|
@ -29,4 +30,9 @@ internal interface TangemPayModelModule {
|
|||
@IntoMap
|
||||
@ClassKey(TangemPayTxHistoryDetailsModel::class)
|
||||
fun bindTangemPayTxHistoryDetailsModel(model: TangemPayTxHistoryDetailsModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(TangemPayChangePinModel::class)
|
||||
fun bindTangemPayChangePinModel(model: TangemPayChangePinModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.tangempay.entity
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class TangemPayChangePinUM(
|
||||
val pinCode: String,
|
||||
val error: TextReference?,
|
||||
val onPinCodeChange: (String) -> Unit,
|
||||
val submitButtonLoading: Boolean,
|
||||
val submitButtonEnabled: Boolean,
|
||||
val onSubmitClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.tangem.features.tangempay.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.domain.pay.model.SetPinResult
|
||||
import com.tangem.domain.pay.repository.CardDetailsRepository
|
||||
import com.tangem.features.tangempay.entity.TangemPayChangePinUM
|
||||
import kotlinx.coroutines.flow.*
|
||||
import com.tangem.features.tangempay.model.transformers.PinCodeChangeTransformer
|
||||
import com.tangem.features.tangempay.navigation.TangemPayDetailsInnerRoute
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.transformer.update
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class TangemPayChangePinModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val cardDetailsRepository: CardDetailsRepository,
|
||||
) : Model() {
|
||||
|
||||
val uiState: StateFlow<TangemPayChangePinUM>
|
||||
field = MutableStateFlow(getInitialState())
|
||||
|
||||
private fun onPinCodeChange(pin: String) {
|
||||
uiState.update(transformer = PinCodeChangeTransformer(newPin = pin))
|
||||
}
|
||||
|
||||
private fun onClickSubmit() {
|
||||
modelScope.launch {
|
||||
uiState.update { it.copy(submitButtonLoading = true) }
|
||||
val result = cardDetailsRepository.setPin(uiState.value.pinCode).getOrNull()
|
||||
uiState.update { it.copy(submitButtonLoading = false) }
|
||||
when (result) {
|
||||
SetPinResult.SUCCESS -> router.push(TangemPayDetailsInnerRoute.ChangePINSuccess)
|
||||
SetPinResult.PIN_TOO_WEAK,
|
||||
SetPinResult.DECRYPTION_ERROR,
|
||||
SetPinResult.UNKNOWN_ERROR,
|
||||
null,
|
||||
-> Unit // TODO: [REDACTED_TASK_KEY] - add error handling once the requirements arrive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInitialState(): TangemPayChangePinUM {
|
||||
return TangemPayChangePinUM(
|
||||
pinCode = "",
|
||||
onPinCodeChange = ::onPinCodeChange,
|
||||
onSubmitClick = ::onClickSubmit,
|
||||
submitButtonEnabled = false,
|
||||
submitButtonLoading = false,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation
|
|||
import com.tangem.features.tangempay.entity.TangemPayDetailsStateFactory
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
|
||||
import com.tangem.features.tangempay.model.transformers.*
|
||||
import com.tangem.features.tangempay.navigation.TangemPayDetailsInnerRoute
|
||||
import com.tangem.features.tangempay.utils.TangemPayErrorMessageFactory
|
||||
import com.tangem.features.tangempay.utils.TangemPayTxHistoryUiActions
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -75,7 +76,7 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onClickChangePin() {
|
||||
// TODO [REDACTED_JIRA]
|
||||
router.push(TangemPayDetailsInnerRoute.ChangePIN)
|
||||
}
|
||||
|
||||
private fun onClickFreezeCard() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.features.tangempay.model.transformers
|
||||
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.TangemPayChangePinUM
|
||||
import com.tangem.features.tangempay.utils.PinCodeValidation
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class PinCodeChangeTransformer(
|
||||
private val newPin: String,
|
||||
) : Transformer<TangemPayChangePinUM> {
|
||||
|
||||
override fun transform(prevState: TangemPayChangePinUM): TangemPayChangePinUM {
|
||||
val valid = PinCodeValidation.validate(pinCode = newPin)
|
||||
// Do not show error yet if user didn't fill all 4 spaces
|
||||
val isError = PinCodeValidation.validateLength(pinCode = newPin) && !valid
|
||||
return prevState.copy(
|
||||
pinCode = newPin,
|
||||
submitButtonEnabled = valid,
|
||||
error = if (isError) {
|
||||
resourceReference(R.string.visa_onboarding_pin_validation_error_message)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,4 +10,7 @@ internal sealed class TangemPayDetailsInnerRoute : Route {
|
|||
|
||||
@Serializable
|
||||
data object ChangePIN : TangemPayDetailsInnerRoute()
|
||||
|
||||
@Serializable
|
||||
data object ChangePINSuccess : TangemPayDetailsInnerRoute()
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
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.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.airbnb.lottie.compose.LottieAnimation
|
||||
import com.airbnb.lottie.compose.LottieCompositionSpec
|
||||
import com.airbnb.lottie.compose.animateLottieCompositionAsState
|
||||
import com.airbnb.lottie.compose.rememberLottieComposition
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
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.TangemThemePreview
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun TangemPayChangePinCodeSuccessScreen(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
val composition by rememberLottieComposition(spec = LottieCompositionSpec.RawRes(R.raw.anim_confetti))
|
||||
val progress by animateLottieCompositionAsState(composition)
|
||||
var showConfetti by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.navigationBarsPadding(),
|
||||
) {
|
||||
TangemTopAppBar(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
title = resourceReference(R.string.common_done).resolveReference(),
|
||||
startButton = null,
|
||||
titleAlignment = Alignment.CenterHorizontally,
|
||||
)
|
||||
Column(
|
||||
modifier
|
||||
.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
SuccessContent(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
)
|
||||
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
.navigationBarsPadding(),
|
||||
text = stringResourceSafe(R.string.common_done),
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
if (showConfetti) {
|
||||
FullScreen(notTouchable = true) {
|
||||
LottieAnimation(
|
||||
composition = composition,
|
||||
progress = { progress },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
showConfetti = true
|
||||
}
|
||||
|
||||
LaunchedEffect(progress == 1f) {
|
||||
if (progress == 1f) {
|
||||
showConfetti = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SuccessContent(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_success_blue_76),
|
||||
tint = Color.Unspecified,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(76.dp),
|
||||
)
|
||||
SpacerH32()
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 32.dp),
|
||||
text = stringResourceSafe(R.string.tangempay_card_details_change_pin_success_title),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
SpacerH12()
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 32.dp),
|
||||
text = stringResourceSafe(R.string.tangempay_card_details_change_pin_success_description),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
SpacerH(72.dp)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
TangemPayChangePinCodeSuccessScreen(onClick = {})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
package com.tangem.features.tangempay.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Color.Companion.Transparent
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.SpacerH4
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.TangemPayChangePinUM
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
internal fun TangemPayChangePinScreen(
|
||||
state: TangemPayChangePinUM,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.navigationBarsPadding(),
|
||||
) {
|
||||
AppBarWithBackButton(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 48.dp)
|
||||
.padding(horizontal = 36.dp)
|
||||
.weight(1f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.visa_onboarding_pin_code_title),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
SpacerH16()
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.visa_onboarding_pin_code_description),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
SpacerH(26.dp)
|
||||
|
||||
PinCodeSection(state)
|
||||
}
|
||||
|
||||
NavigationPrimaryButton(
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
primaryButton = NavigationButton(
|
||||
textReference = resourceReference(R.string.common_submit),
|
||||
onClick = state.onSubmitClick,
|
||||
shouldShowProgress = state.submitButtonLoading,
|
||||
isEnabled = state.submitButtonEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PinCodeSection(state: TangemPayChangePinUM, modifier: Modifier = Modifier) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
PinCode(
|
||||
modifier = modifier,
|
||||
value = state.pinCode,
|
||||
onValueChange = state.onPinCodeChange,
|
||||
focusRequester = focusRequester,
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = state.error != null,
|
||||
) {
|
||||
val error = remember(this) { requireNotNull(state.error) }
|
||||
Column {
|
||||
SpacerH4()
|
||||
Text(
|
||||
text = error.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.warning,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
delay(timeMillis = 300)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PinCode(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
numbersCount: Int = 4,
|
||||
focusRequester: FocusRequester = remember { FocusRequester() },
|
||||
) {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = { text ->
|
||||
if (text.length <= numbersCount) {
|
||||
onValueChange(text)
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
.focusRequester(focusRequester)
|
||||
.clickable {
|
||||
focusRequester.requestFocus()
|
||||
keyboardController?.show()
|
||||
},
|
||||
textStyle = TangemTheme.typography.h1.copy(color = Transparent),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.NumberPassword,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }),
|
||||
cursorBrush = SolidColor(Transparent),
|
||||
decorationBox = { innerTextField ->
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
repeat(numbersCount) { index ->
|
||||
val digit = value.getOrNull(index)?.toString()
|
||||
PinDigitBox(
|
||||
digit = digit,
|
||||
backgroundColor = TangemTheme.colors.field.focused,
|
||||
borderColor = TangemTheme.colors.stroke.primary,
|
||||
textColor = TangemTheme.colors.text.primary1,
|
||||
textStyle = TangemTheme.typography.h1,
|
||||
)
|
||||
}
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PinDigitBox(
|
||||
digit: String?,
|
||||
backgroundColor: Color,
|
||||
borderColor: Color,
|
||||
textColor: Color,
|
||||
textStyle: TextStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(width = 48.dp, height = 64.dp)
|
||||
.background(
|
||||
color = backgroundColor,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = borderColor,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (digit != null) {
|
||||
Text(
|
||||
text = digit,
|
||||
style = textStyle,
|
||||
color = textColor,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.features.tangempay.utils
|
||||
|
||||
internal object PinCodeValidation {
|
||||
|
||||
private const val PIN_CODE_LENGTH = 4
|
||||
|
||||
fun validate(pinCode: String): Boolean {
|
||||
return sequenceOf(
|
||||
::validateAllDigits,
|
||||
::validateLength,
|
||||
::validateNoRepeatedDigits,
|
||||
::validateNoConsecutiveDigits,
|
||||
).all { it(pinCode) }
|
||||
}
|
||||
|
||||
fun validateLength(pinCode: String): Boolean {
|
||||
return pinCode.length == PIN_CODE_LENGTH
|
||||
}
|
||||
|
||||
private fun validateAllDigits(pinCode: String): Boolean {
|
||||
return pinCode.all { it.isDigit() }
|
||||
}
|
||||
|
||||
private fun validateNoRepeatedDigits(pinCode: String): Boolean {
|
||||
return pinCode.toSet().size > 1
|
||||
}
|
||||
|
||||
private fun validateNoConsecutiveDigits(pinCode: String): Boolean {
|
||||
return pinCode.zipWithNext().any { it.second != it.first + 1 } &&
|
||||
pinCode.zipWithNext().any { it.second != it.first - 1 }
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue