Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-17 17:01:49 +05:00
parent 5e9771c378
commit ca03f18d81
11 changed files with 363 additions and 4 deletions

View file

@ -107,7 +107,7 @@ private fun CellDecoration(
Box(
modifier = Modifier
.background(
color = TangemTheme.colors.background.tertiary,
color = TangemTheme.colors.field.primary,
shape = RoundedCornerShape(8.dp),
),
contentAlignment = Alignment.Center,

View file

@ -6,6 +6,7 @@ import com.arkivanov.decompose.router.stack.push
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.navigation.Router
import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeComponent
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletRoute
@ -22,6 +23,7 @@ internal class AddExistingWalletModel @Inject constructor(
val addExistingWalletStartModelCallbacks = AddExistingWalletStartModelCallbacks()
val addExistingWalletImportModelCallbacks = AddExistingWalletImportModelCallbacks()
val pushNotificationsComponentModelCallbacks = PushNotificationsComponentModelCallbacks()
val accessCodeModelCallbacks = AccessCodeModelCallbacks()
val stackNavigation = StackNavigation<AddExistingWalletRoute>()
@ -46,4 +48,14 @@ internal class AddExistingWalletModel @Inject constructor(
// TODO [REDACTED_TASK_KEY]
}
}
inner class AccessCodeModelCallbacks : SetAccessCodeComponent.ModelCallbacks {
override fun onBackClick() {
// TODO [REDACTED_TASK_KEY]
}
override fun onAccessCodeSet() {
// TODO [REDACTED_TASK_KEY]
}
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.features.hotwallet.addexistingwallet.root.di
import com.tangem.core.decompose.model.Model
import com.tangem.features.hotwallet.AddExistingWalletComponent
import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeModel
import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel
import com.tangem.features.hotwallet.addexistingwallet.root.DefaultAddExistingWalletComponent
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartModel
@ -42,4 +43,9 @@ internal interface AddExistingWalletModuleBinds {
@IntoMap
@ClassKey(AddExistingWalletImportModel::class)
fun bindAddExistingWalletImportModel(model: AddExistingWalletImportModel): Model
@Binds
@IntoMap
@ClassKey(SetAccessCodeModel::class)
fun bindAccessCodeModel(model: SetAccessCodeModel): Model
}

View file

@ -2,6 +2,7 @@ package com.tangem.features.hotwallet.addexistingwallet.root.routing
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeComponent
import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
@ -36,6 +37,12 @@ internal class AddExistingWalletChildFactory @Inject constructor(
callbacks = model.pushNotificationsComponentModelCallbacks,
),
)
is AddExistingWalletRoute.AccessCode -> SetAccessCodeComponent(
context = childContext,
params = SetAccessCodeComponent.Params(
callbacks = model.accessCodeModelCallbacks,
),
)
}
}
}

View file

@ -13,4 +13,7 @@ internal sealed class AddExistingWalletRoute : Route {
@Serializable
object PushNotifications : AddExistingWalletRoute()
@Serializable
object AccessCode : AddExistingWalletRoute()
}

View file

@ -0,0 +1,43 @@
package com.tangem.features.hotwallet.setaccesscode
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.hotwallet.setaccesscode.ui.SetAccessCodeContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
internal class SetAccessCodeComponent @AssistedInject constructor(
@Assisted private val context: AppComponentContext,
@Assisted private val params: Params,
) : ComposableContentComponent, AppComponentContext by context {
private val model: SetAccessCodeModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
SetAccessCodeContent(
state = state,
onBack = { model.onBack() },
modifier = modifier,
)
DisableScreenshotsDisposableEffect()
}
interface ModelCallbacks {
fun onBackClick()
fun onAccessCodeSet()
}
data class Params(
val callbacks: ModelCallbacks,
)
}

View file

@ -0,0 +1,84 @@
package com.tangem.features.hotwallet.setaccesscode
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
@Stable
@ModelScoped
internal class SetAccessCodeModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val params = paramsContainer.require<SetAccessCodeComponent.Params>()
internal val uiState: StateFlow<SetAccessCodeUM>
field = MutableStateFlow(getInitialState())
fun onBack() {
when (uiState.value.step) {
SetAccessCodeUM.Step.AccessCode -> {
params.callbacks.onBackClick()
}
SetAccessCodeUM.Step.ConfirmAccessCode -> {
uiState.update {
it.copy(
step = SetAccessCodeUM.Step.AccessCode,
accessCodeSecond = "",
)
}
}
}
}
private fun getInitialState() = SetAccessCodeUM(
step = SetAccessCodeUM.Step.AccessCode,
accessCodeFirst = "",
accessCodeSecond = "",
onAccessCodeFirstChange = ::onAccessCodeFirstChange,
onAccessCodeSecondChange = ::onAccessCodeSecondChange,
buttonEnabled = false,
onContinue = ::onContinue,
)
private fun onAccessCodeFirstChange(value: String) {
uiState.update {
it.copy(
accessCodeFirst = value,
buttonEnabled = value.length == uiState.value.accessCodeLength,
)
}
}
private fun onAccessCodeSecondChange(value: String) {
uiState.update {
it.copy(
accessCodeSecond = value,
buttonEnabled = uiState.value.accessCodeFirst == uiState.value.accessCodeSecond,
)
}
}
private fun onContinue() {
when (uiState.value.step) {
SetAccessCodeUM.Step.AccessCode -> {
uiState.update {
it.copy(
step = SetAccessCodeUM.Step.ConfirmAccessCode,
)
}
}
SetAccessCodeUM.Step.ConfirmAccessCode -> {
params.callbacks.onAccessCodeSet()
}
}
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.features.hotwallet.setaccesscode.entity
internal data class SetAccessCodeUM(
val step: Step,
val accessCodeFirst: String,
val accessCodeSecond: String,
val onAccessCodeFirstChange: (String) -> Unit,
val onAccessCodeSecondChange: (String) -> Unit,
val buttonEnabled: Boolean,
val onContinue: () -> Unit,
) {
val accessCodeLength: Int = ACCESS_CODE_LENGTH
enum class Step {
AccessCode,
ConfirmAccessCode,
}
companion object {
private const val ACCESS_CODE_LENGTH = 6
}
}

View file

@ -0,0 +1,59 @@
package com.tangem.features.hotwallet.setaccesscode.ui
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemAnimations
import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM
import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM.Step.*
import com.tangem.core.res.R
@Composable
internal fun SetAccessCodeContent(state: SetAccessCodeUM, onBack: () -> Unit, modifier: Modifier = Modifier) {
BackHandler(onBack = onBack)
Column(
modifier = modifier
.fillMaxSize()
.navigationBarsPadding(),
) {
AnimatedContent(
modifier = Modifier.weight(1f),
targetState = state.step,
transitionSpec = TangemAnimations.AnimatedContent
.slide { initial, target -> target.ordinal > initial.ordinal },
label = "AnimatedContent",
) { step ->
when (step) {
AccessCode -> SetAccessCodeEnter(
modifier = Modifier.padding(top = 16.dp),
state = state,
reEnterAccessCodeState = false,
)
ConfirmAccessCode -> SetAccessCodeEnter(
modifier = Modifier.padding(top = 16.dp),
state = state,
reEnterAccessCodeState = true,
)
}
}
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.imePadding(),
text = if (state.step == ConfirmAccessCode) {
stringResourceSafe(R.string.common_confirm)
} else {
stringResourceSafe(R.string.common_continue)
},
onClick = state.onContinue,
)
}
}

View file

@ -0,0 +1,125 @@
package com.tangem.features.hotwallet.setaccesscode.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
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.fields.PinTextField
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.hotwallet.setaccesscode.entity.SetAccessCodeUM
@Composable
internal fun SetAccessCodeEnter(
state: SetAccessCodeUM,
reEnterAccessCodeState: Boolean,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxSize()
.background(TangemTheme.colors.background.primary)
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier
.padding(top = 56.dp)
.align(Alignment.CenterHorizontally),
text = if (reEnterAccessCodeState) {
stringResourceSafe(R.string.access_code_confirm_title)
} else {
stringResourceSafe(R.string.access_code_create_title)
},
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
Text(
modifier = Modifier
.padding(16.dp)
.align(Alignment.CenterHorizontally),
text = if (reEnterAccessCodeState) {
stringResourceSafe(R.string.access_code_confirm_description)
} else {
stringResourceSafe(
R.string.access_code_create_description,
state.accessCodeLength,
)
},
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
contentAlignment = Alignment.Center,
) {
PinTextField(
length = state.accessCodeLength,
isPasswordVisual = true,
value = if (reEnterAccessCodeState) {
state.accessCodeSecond
} else {
state.accessCodeFirst
},
onValueChange = if (reEnterAccessCodeState) {
state.onAccessCodeSecondChange
} else {
state.onAccessCodeFirstChange
},
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
SetAccessCodeEnter(
reEnterAccessCodeState = false,
state = SetAccessCodeUM(
step = SetAccessCodeUM.Step.AccessCode,
accessCodeFirst = "",
accessCodeSecond = "",
onAccessCodeFirstChange = {},
onAccessCodeSecondChange = {},
buttonEnabled = false,
onContinue = {},
),
)
}
}
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview2() {
TangemThemePreview {
SetAccessCodeEnter(
reEnterAccessCodeState = true,
state = SetAccessCodeUM(
step = SetAccessCodeUM.Step.ConfirmAccessCode,
accessCodeFirst = "",
accessCodeSecond = "",
onAccessCodeFirstChange = {},
onAccessCodeSecondChange = {},
buttonEnabled = false,
onContinue = {},
),
)
}
}

View file

@ -71,13 +71,11 @@ internal fun MultiWalletAccessCodeEnter(
state.onAccessCodeFirstChange
},
label = stringResourceSafe(id = R.string.onboarding_wallet_info_title_third),
isError = state.codesNotMatchError || state.atLeast4CharError,
isError = state.codesNotMatchError,
visualTransformation = PasswordVisualTransformation(),
caption = when {
state.codesNotMatchError && reEnterAccessCodeState ->
stringResourceSafe(R.string.onboarding_access_codes_doesnt_match)
state.atLeast4CharError && !reEnterAccessCodeState ->
stringResourceSafe(R.string.onboarding_access_code_too_short)
else -> null
},
)