Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-22 14:51:20 +03:00
parent 52eddd3ac6
commit c07ecae962
14 changed files with 258 additions and 35 deletions

View file

@ -44,6 +44,7 @@ fun OutlineTextField(
isError: Boolean = false,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions(),
visualTransformation: VisualTransformation = VisualTransformation.None,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
TangemTextField(
@ -58,6 +59,7 @@ fun OutlineTextField(
isError = isError,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
visualTransformation = visualTransformation,
interactionSource = interactionSource,
)
}
@ -76,6 +78,8 @@ fun OutlineTextFieldWithIcon(
isError: Boolean = false,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions(),
visualTransformation: VisualTransformation = VisualTransformation.None,
caption: String? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
TangemTextFieldWithIcon(
@ -91,6 +95,8 @@ fun OutlineTextFieldWithIcon(
isError = isError,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
visualTransformation = visualTransformation,
caption = caption,
interactionSource = interactionSource,
onIconClick = onIconClick,
)
@ -189,15 +195,16 @@ private fun TangemTextField(
enter = fadeIn(),
exit = fadeOut(),
) {
if (caption.isNullOrEmpty()) return@AnimatedVisibility
val captionWrapped = remember(this) { requireNotNull(caption) }
Column {
Spacer(modifier = Modifier.height(4.dp))
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
text = caption,
style = TangemTheme.typography.body1,
text = captionWrapped,
style = TangemTheme.typography.caption2,
color = colors.captionColor(enabled = enabled, isError = isError).value,
)
}
@ -218,6 +225,7 @@ private fun TangemTextFieldWithIcon(
placeholder: String? = null,
enabled: Boolean = true,
isError: Boolean = false,
caption: String? = null,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions(),
@ -280,6 +288,26 @@ private fun TangemTextFieldWithIcon(
}
},
)
AnimatedVisibility(
visible = !caption.isNullOrEmpty(),
enter = fadeIn(),
exit = fadeOut(),
) {
val captionWrapped = remember(this) { requireNotNull(caption) }
Column {
Spacer(modifier = Modifier.height(4.dp))
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
text = captionWrapped,
style = TangemTheme.typography.caption2,
color = colors.captionColor(enabled = enabled, isError = isError).value,
)
}
}
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,9C12.796,9 13.559,9.316 14.121,9.879C14.684,10.441 15,11.204 15,12C15,12.796 14.684,13.559 14.121,14.121C13.559,14.684 12.796,15 12,15C11.204,15 10.441,14.684 9.879,14.121C9.316,13.559 9,12.796 9,12C9,11.204 9.316,10.441 9.879,9.879C10.441,9.316 11.204,9 12,9ZM12,4.5C17,4.5 21.27,7.61 23,12C21.27,16.39 17,19.5 12,19.5C7,19.5 2.73,16.39 1,12C2.73,7.61 7,4.5 12,4.5ZM3.18,12C4.83,15.36 8.24,17.5 12,17.5C15.76,17.5 19.17,15.36 20.82,12C19.17,8.64 15.76,6.5 12,6.5C8.24,6.5 4.83,8.64 3.18,12Z"
android:fillColor="#1E1E1E"/>
</vector>

View file

@ -38,8 +38,8 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.ui.WalletArtworksState
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor(
@Assisted private val context: AppComponentContext,
@ -55,7 +55,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
ChooseBackupOption -> WalletArtworksState.Fan
SeedPhrase -> WalletArtworksState.Folded
AddBackupDevice -> WalletArtworksState.Unfold(WalletArtworksState.Unfold.Step.First)
FinishBackup -> WalletArtworksState.Stack()
Finalize -> WalletArtworksState.Stack()
Done -> error("Done state should not be used as starting state")
},
)
@ -107,6 +107,14 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
)
init {
componentScope.launch {
// access code bs result
childParams.multiWalletState
.map { it.accessCode }
.distinctUntilChanged()
.filter { it != null }
.collectLatest { handleNavigationEvent(Finalize) }
}
bottomSheetNavigation.navigate {}
}
@ -144,7 +152,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
),
onEvent = ::handleBackupComponentEvent,
)
FinishBackup -> TODO()
Finalize -> TODO()
Done -> TODO()
}
}
@ -154,12 +162,12 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
ChooseBackupOption -> {
artworksState.value = WalletArtworksState.Fan
}
SeedPhrase -> {
}
SeedPhrase -> {}
AddBackupDevice -> {
artworksState.value = WalletArtworksState.Unfold(WalletArtworksState.Unfold.Step.First)
}
FinishBackup -> {
Finalize -> {
artworksState.value = WalletArtworksState.Stack()
}
Done -> {
// TODO
@ -172,7 +180,8 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
private fun handleBackupComponentEvent(event: MultiWalletBackupComponent.Event) {
when (event) {
MultiWalletBackupComponent.Event.Done -> {
// TODO navigate to finalize
artworksState.value = WalletArtworksState.Stack()
bottomSheetNavigation.navigate { }
}
MultiWalletBackupComponent.Event.OneDeviceAdded -> {
artworksState.value = WalletArtworksState.Unfold(WalletArtworksState.Unfold.Step.Second)

View file

@ -12,36 +12,47 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.model.MultiWalletAccessCodeModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.ui.MultiWalletAccessCodeBS
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
@Suppress("UnusedPrivateMember")
class MultiWalletAccessCodeComponent(
context: AppComponentContext,
private val params: MultiWalletChildParams,
params: MultiWalletChildParams,
private val onDismiss: () -> Unit,
) : AppComponentContext by context, ComposableBottomSheetComponent {
private val model = getOrCreateModel<MultiWalletAccessCodeModel>()
private val model: MultiWalletAccessCodeModel = getOrCreateModel(params)
private val showBs = MutableStateFlow(true)
init {
componentScope.launch {
model.onDismiss.collect {
showBs.value = false
delay(timeMillis = 500)
onDismiss()
}
}
}
override fun dismiss() {
onDismiss()
componentScope.launch {
showBs.value = false
delay(timeMillis = 500)
onDismiss()
}
}
@Composable
override fun BottomSheet() {
val state by model.uiState.collectAsStateWithLifecycle()
val bsConfig = remember(this) {
val showBs by showBs.collectAsStateWithLifecycle()
val bsConfig = remember(this, showBs) {
TangemBottomSheetConfig(
isShow = true,
onDismissRequest = onDismiss,
isShow = showBs,
onDismissRequest = { dismiss() },
content = TangemBottomSheetConfigContent.Empty,
)
}

View file

@ -1,8 +1,12 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.model
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.ui.state.MultiWalletAccessCodeUM
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@ -11,16 +15,26 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
private const val MINIMUM_ACCESS_CODE_LENGTH = 4
@ComponentScoped
internal class MultiWalletAccessCodeModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val _uiState = MutableStateFlow(getInitialState())
private val params = paramsContainer.require<MultiWalletChildParams>()
val uiState = _uiState.asStateFlow()
val onDismiss = MutableSharedFlow<Unit>()
init {
params.multiWalletState.update {
it.copy(accessCode = null)
}
}
fun onBack() {
when (_uiState.value.step) {
MultiWalletAccessCodeUM.Step.Intro -> {
@ -28,33 +42,93 @@ internal class MultiWalletAccessCodeModel @Inject constructor(
}
MultiWalletAccessCodeUM.Step.AccessCode -> {
_uiState.update {
it.copy(step = MultiWalletAccessCodeUM.Step.Intro)
it.copy(
step = MultiWalletAccessCodeUM.Step.Intro,
accessCodeFirst = TextFieldValue(""),
accessCodeSecond = TextFieldValue(""),
)
}
}
MultiWalletAccessCodeUM.Step.ConfirmAccessCode -> {
_uiState.update {
it.copy(step = MultiWalletAccessCodeUM.Step.AccessCode)
it.copy(
step = MultiWalletAccessCodeUM.Step.AccessCode,
accessCodeSecond = TextFieldValue(""),
)
}
}
}
}
private fun getInitialState() = MultiWalletAccessCodeUM(
onAccessCodeFirstChange = ::onAccessCodeFirstChange,
onAccessCodeSecondChange = ::onAccessCodeSecondChange,
onContinue = ::onContinue,
onAccessCodeHideClick = ::onAccessCodeHideClick,
)
private fun onAccessCodeHideClick() {
_uiState.update {
it.copy(accessCodeHidden = !it.accessCodeHidden)
}
}
private fun onAccessCodeFirstChange(textFieldValue: TextFieldValue) {
_uiState.update {
it.copy(
accessCodeFirst = textFieldValue,
atLeast4CharError = false,
)
}
}
private fun onAccessCodeSecondChange(textFieldValue: TextFieldValue) {
_uiState.update {
it.copy(
accessCodeSecond = textFieldValue,
codesNotMatchError = false,
)
}
}
private fun onContinue() {
when (_uiState.value.step) {
MultiWalletAccessCodeUM.Step.Intro -> {
_uiState.update { it.copy(step = MultiWalletAccessCodeUM.Step.AccessCode) }
}
MultiWalletAccessCodeUM.Step.AccessCode -> {
_uiState.update { it.copy(step = MultiWalletAccessCodeUM.Step.ConfirmAccessCode) }
if (_uiState.value.accessCodeFirst.text.length >= MINIMUM_ACCESS_CODE_LENGTH) {
_uiState.update {
it.copy(
step = MultiWalletAccessCodeUM.Step.ConfirmAccessCode,
atLeast4CharError = false,
)
}
} else {
_uiState.update {
it.copy(atLeast4CharError = true)
}
}
}
MultiWalletAccessCodeUM.Step.ConfirmAccessCode -> {
// TODO check
modelScope.launch { onDismiss.emit(Unit) }
if (checkAccessCode()) {
params.multiWalletState.update {
it.copy(accessCode = OnboardingMultiWalletState.AccessCode(uiState.value.accessCodeFirst.text))
}
modelScope.launch { onDismiss.emit(Unit) }
}
}
}
}
private fun checkAccessCode(): Boolean {
if (_uiState.value.accessCodeFirst.text == _uiState.value.accessCodeSecond.text) {
return true
} else {
_uiState.update {
it.copy(codesNotMatchError = true)
}
return false
}
}
}

View file

@ -3,16 +3,23 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.ui
import androidx.compose.foundation.layout.*
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.res.stringResource
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.OutlineTextField
import com.tangem.core.ui.components.OutlineTextFieldWithIcon
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.ui.state.MultiWalletAccessCodeUM
import kotlinx.coroutines.delay
@Composable
internal fun MultiWalletAccessCodeEnter(
@ -43,8 +50,19 @@ internal fun MultiWalletAccessCodeEnter(
style = TangemTheme.typography.body1,
)
OutlineTextField(
modifier = modifier.fillMaxWidth(),
val focusRequester = remember { FocusRequester() }
OutlineTextFieldWithIcon(
modifier = modifier
.focusRequester(focusRequester)
.fillMaxWidth(),
iconResId = if (state.accessCodeHidden) {
R.drawable.ic_eye_outline_24
} else {
R.drawable.ic_eye_off_outline_24
},
iconColor = TangemTheme.colors.icon.primary1,
onIconClick = state.onAccessCodeHideClick,
value = if (reEnterAccessCodeState) {
state.accessCodeSecond
} else {
@ -56,13 +74,25 @@ internal fun MultiWalletAccessCodeEnter(
state.onAccessCodeFirstChange
},
label = stringResource(id = R.string.onboarding_wallet_info_title_third),
isError = state.codesNotMatchError,
caption = if (state.codesNotMatchError && reEnterAccessCodeState) {
stringResource(R.string.onboarding_access_codes_doesnt_match)
isError = state.codesNotMatchError || state.atLeast4CharError,
visualTransformation = if (state.accessCodeHidden) {
PasswordVisualTransformation()
} else {
null
VisualTransformation.None
},
caption = when {
state.codesNotMatchError && reEnterAccessCodeState ->
stringResource(R.string.onboarding_access_codes_doesnt_match)
state.atLeast4CharError && !reEnterAccessCodeState ->
stringResource(R.string.onboarding_access_code_too_short)
else -> null
},
)
LaunchedEffect(Unit) {
delay(timeMillis = 200)
focusRequester.requestFocus()
}
}
}

View file

@ -7,9 +7,12 @@ internal data class MultiWalletAccessCodeUM(
val accessCodeFirst: TextFieldValue = TextFieldValue(""),
val accessCodeSecond: TextFieldValue = TextFieldValue(""),
val codesNotMatchError: Boolean = false,
val atLeast4CharError: Boolean = false,
val accessCodeHidden: Boolean = true,
val onAccessCodeFirstChange: (TextFieldValue) -> Unit = {},
val onAccessCodeSecondChange: (TextFieldValue) -> Unit = {},
val onContinue: () -> Unit = {},
val onAccessCodeHideClick: () -> Unit = {},
) {
enum class Step {
Intro,

View file

@ -0,0 +1,22 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
import kotlinx.coroutines.flow.SharedFlow
@Suppress("UnusedPrivateMember")
internal class MultiWalletFinalizeComponent(
context: AppComponentContext,
params: MultiWalletChildParams,
onNextStep: (OnboardingMultiWalletState.Step) -> Unit,
backButtonClickFlow: SharedFlow<Unit>,
) : AppComponentContext by context, MultiWalletChildComponent {
@Composable
override fun Content(modifier: Modifier) {
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.state
data class MultiWalletFinalizeUM(
val step: Step = Step.Device1,
val cardNumber: String = "",
) {
enum class Step {
Device1, Device2, Device3
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.state.MultiWalletFinalizeUM
@Suppress("UnusedPrivateMember")
@Composable
fun MultiWalletFinalize(state: MultiWalletFinalizeUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.navigationBarsPadding(),
verticalArrangement = Arrangement.Bottom,
) {
// TODO
}
}

View file

@ -12,7 +12,7 @@ internal fun MultiWalletSeedPhrase(state: MultiWalletSeedPhraseUM, modifier: Mod
AnimatedContent(
modifier = modifier.navigationBarsPadding(),
targetState = state,
transitionSpec = TangemAnimations.AnimatedContent.slide { from, to -> to.order > from.order },
transitionSpec = TangemAnimations.AnimatedContent.slide { from, to -> to.order < from.order },
contentKey = { st -> st::class.java },
label = "animatedContent",
) { st ->

View file

@ -25,6 +25,7 @@ internal class OnboardingMultiWalletModel @Inject constructor(
OnboardingMultiWalletState(
currentStep = getInitialStep(),
currentScanResponse = params.scanResponse,
accessCode = null,
),
)
@ -47,7 +48,7 @@ internal class OnboardingMultiWalletModel @Inject constructor(
card.wallets.isNotEmpty() && card.backupStatus == CardDTO.BackupStatus.NoBackup ->
OnboardingMultiWalletState.Step.AddBackupDevice
card.wallets.isNotEmpty() && card.backupStatus?.isActive == true ->
OnboardingMultiWalletState.Step.FinishBackup
OnboardingMultiWalletState.Step.Finalize
else ->
OnboardingMultiWalletState.Step.CreateWallet
}

View file

@ -4,20 +4,24 @@ import com.tangem.domain.models.scan.ScanResponse
data class OnboardingMultiWalletState(
val currentStep: Step,
val accessCode: AccessCode?,
val currentScanResponse: ScanResponse,
) {
/**
* Wallet1
* CreateWallet -> ChooseBackupOption -> AddBackupDevice -> FinishBackup -> [Done]
* CreateWallet -> ChooseBackupOption -> AddBackupDevice -> Finalize -> [Done]
* |
* -> [Done]
*
* Wallet2/Ring
* CreateWallet -> SeedPhrase -> AddBackupDevice -> FinishBackup -> [Done]
* |
* -> AddBackupDevice -> FinishBackup -> [Done]
* -> AddBackupDevice -> Finalize -> [Done]
*/
enum class Step {
CreateWallet, ChooseBackupOption, SeedPhrase, AddBackupDevice, FinishBackup, Done
CreateWallet, ChooseBackupOption, SeedPhrase, AddBackupDevice, Finalize, Done
}
@JvmInline
value class AccessCode(val accessCode: String)
}

View file

@ -11,6 +11,6 @@ fun screenTitleByStep(step: OnboardingMultiWalletState.Step): TextReference = wh
OnboardingMultiWalletState.Step.SeedPhrase -> TODO()
OnboardingMultiWalletState.Step.AddBackupDevice ->
resourceReference(R.string.onboarding_navbar_title_creating_backup)
OnboardingMultiWalletState.Step.FinishBackup -> TODO()
OnboardingMultiWalletState.Step.Finalize -> TODO()
OnboardingMultiWalletState.Step.Done -> TODO()
}