Updated on 2026-08-14
This commit is contained in:
parent
ca35718e95
commit
78e0d5e3b3
2 changed files with 381 additions and 0 deletions
|
|
@ -0,0 +1,138 @@
|
|||
package com.tangem.feature.onboarding.presentation.wallet2.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.OutlineTextField
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconLeft
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.model.CheckSeedPhraseState
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingDescription
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.model.TextFieldState
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun CheckSeedPhraseScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
state: CheckSeedPhraseState = CheckSeedPhraseState(),
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
item {
|
||||
OnboardingDescriptionBlock(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(
|
||||
start = TangemTheme.dimens.size16,
|
||||
top = TangemTheme.dimens.size48,
|
||||
end = TangemTheme.dimens.size16,
|
||||
),
|
||||
descriptionsList = listOf(
|
||||
OnboardingDescription(
|
||||
// FIXME: replace by string res
|
||||
title = "So, let’s check",
|
||||
subTitle = "To check wether you’ve written down your seed phrase correctly, please enter the 2nd, 7th and 11th words",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
item {
|
||||
CheckSeedPhraseBlock(
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
.weight(1f)
|
||||
.padding(
|
||||
start = TangemTheme.dimens.size16,
|
||||
top = TangemTheme.dimens.size20,
|
||||
end = TangemTheme.dimens.size16,
|
||||
),
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
}
|
||||
OnboardingActionBlock(
|
||||
firstActionContent = {
|
||||
PrimaryButtonIconLeft(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.size16),
|
||||
text = stringResource(id = R.string.onboarding_create_wallet_button_create_wallet),
|
||||
icon = painterResource(id = R.drawable.ic_tangem_24),
|
||||
enabled = state.buttonCreateWallet.enabled,
|
||||
showProgress = state.buttonCreateWallet.showProgress,
|
||||
onClick = state.buttonCreateWallet.onClick,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CheckSeedPhraseBlock(
|
||||
modifier: Modifier = Modifier,
|
||||
state: CheckSeedPhraseState,
|
||||
) {
|
||||
Column(modifier) {
|
||||
OutlineTextField(
|
||||
value = state.tvSecondPhrase.textFieldValue,
|
||||
onValueChange = state.tvSecondPhrase.onTextFieldValueChanged,
|
||||
label = state.tvSecondPhrase.getLabel(),
|
||||
isError = state.tvSecondPhrase.isError,
|
||||
)
|
||||
|
||||
OutlineTextField(
|
||||
value = state.tvSeventhPhrase.textFieldValue,
|
||||
onValueChange = state.tvSeventhPhrase.onTextFieldValueChanged,
|
||||
label = state.tvSeventhPhrase.getLabel(),
|
||||
isError = state.tvSeventhPhrase.isError,
|
||||
)
|
||||
|
||||
OutlineTextField(
|
||||
value = state.tvEleventhPhrase.textFieldValue,
|
||||
onValueChange = state.tvEleventhPhrase.onTextFieldValueChanged,
|
||||
label = state.tvEleventhPhrase.getLabel(),
|
||||
isError = state.tvEleventhPhrase.isError,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CheckSeedPhraseBlock2(
|
||||
modifier: Modifier = Modifier,
|
||||
state: CheckSeedPhraseState,
|
||||
) {
|
||||
Column(modifier) {
|
||||
listOf(state.tvSecondPhrase, state.tvSeventhPhrase, state.tvEleventhPhrase).forEach { field ->
|
||||
OutlineTextField(
|
||||
value = field.textFieldValue,
|
||||
onValueChange = field.onTextFieldValueChanged,
|
||||
label = field.getLabel(),
|
||||
isError = field.isError,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TextFieldState.getLabel(): String? {
|
||||
return labelRes?.let { stringResource(id = it) } ?: label
|
||||
}
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
package com.tangem.feature.onboarding.presentation.wallet2.viewmodel
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tangem.feature.onboarding.domain.SeedPhraseInteractor
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.model.AboutUiAction
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.model.CheckSeedPhraseUiAction
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.model.ImportSeedPhraseUiAction
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.model.IntroUiAction
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseStep
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.model.SeedPhraseField
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.model.TextFieldState
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.model.TextFieldUiAction
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.model.UiActions
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.model.YourSeedPhraseUiAction
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.ui.StateBuilder
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.Debouncer
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@HiltViewModel
|
||||
class SeedPhraseViewModel @Inject constructor(
|
||||
private val seedPhraseInteractor: SeedPhraseInteractor,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : ViewModel() {
|
||||
|
||||
var aboutSeedPhraseUriProvider: AboutSeedPhraseOpener? = null
|
||||
var chatLauncher: ChatSupportOpener? = null
|
||||
|
||||
private var uiBuilder = StateBuilder(createUiActions())
|
||||
|
||||
var uiState: OnboardingSeedPhraseState by mutableStateOf(uiBuilder.init())
|
||||
private set
|
||||
|
||||
val currentStep: OnboardingSeedPhraseStep
|
||||
get() = uiState.step
|
||||
|
||||
private val textFieldsDebouncers = mutableMapOf<SeedPhraseField, Debouncer>()
|
||||
|
||||
init {
|
||||
prepareCheckYourSeedPhraseTest()
|
||||
}
|
||||
|
||||
// TODO: delete
|
||||
private fun prepareCheckYourSeedPhraseTest() {
|
||||
uiState.copy(step = OnboardingSeedPhraseStep.AboutSeedPhrase)
|
||||
viewModelScope.launchIo {
|
||||
delay(300)
|
||||
buttonGenerateSeedPhraseClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createUiActions(): UiActions = UiActions(
|
||||
introActions = IntroUiAction(
|
||||
buttonCreateWalletClick = ::buttonCreateWalletClick,
|
||||
buttonOtherOptionsClick = ::buttonOtherOptionsClick,
|
||||
),
|
||||
aboutActions = AboutUiAction(
|
||||
buttonReadMoreAboutSeedPhraseClick = ::buttonReadMoreAboutSeedPhraseClick,
|
||||
buttonGenerateSeedPhraseClick = ::buttonGenerateSeedPhraseClick,
|
||||
buttonImportSeedPhraseClick = ::buttonImportSeedPhraseClick,
|
||||
),
|
||||
yourSeedPhraseActions = YourSeedPhraseUiAction(
|
||||
buttonContinueClick = ::buttonContinueClick,
|
||||
),
|
||||
checkSeedPhraseActions = CheckSeedPhraseUiAction(
|
||||
buttonCreateWalletClick = ::buttonCreateWalletWithSeedPhraseClick,
|
||||
secondTextFieldAction = TextFieldUiAction(
|
||||
onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Second, value) },
|
||||
onFocusChanged = { isFocused -> onFocusChanged(SeedPhraseField.Second, isFocused) },
|
||||
),
|
||||
seventhTextFieldAction = TextFieldUiAction(
|
||||
onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Seventh, value) },
|
||||
onFocusChanged = { isFocused -> onFocusChanged(SeedPhraseField.Seventh, isFocused) },
|
||||
),
|
||||
eleventhTextFieldAction = TextFieldUiAction(
|
||||
onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Eleventh, value) },
|
||||
onFocusChanged = { isFocused -> onFocusChanged(SeedPhraseField.Eleventh, isFocused) },
|
||||
),
|
||||
),
|
||||
importSeedPhraseActions = ImportSeedPhraseUiAction(
|
||||
phraseTextFieldAction = TextFieldUiAction(
|
||||
// onFocusChanged = { isFocused -> onFocusChanged(SeedPhraseField.Eleventh, isFocused) },
|
||||
// onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Eleventh, value) },
|
||||
),
|
||||
suggestedPhraseClick = ::buttonSuggestedPhraseClick,
|
||||
buttonCreateWalletClick = ::buttonCreateWalletWithSeedPhraseClick,
|
||||
),
|
||||
menuChatClick = ::menuChatClick,
|
||||
)
|
||||
|
||||
override fun onCleared() {
|
||||
textFieldsDebouncers.forEach { entry -> entry.value.release() }
|
||||
textFieldsDebouncers.clear()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
// region CheckSeedPhrase
|
||||
private fun onFocusChanged(field: SeedPhraseField, isFocused: Boolean) {
|
||||
when (field) {
|
||||
SeedPhraseField.Second -> {}
|
||||
SeedPhraseField.Seventh -> {}
|
||||
SeedPhraseField.Eleventh -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTextFieldChanged(field: SeedPhraseField, textFieldValue: TextFieldValue) {
|
||||
uiState = uiBuilder.checkSeedPhrase.updateTextField(uiState, field, textFieldValue)
|
||||
|
||||
val fieldState = field.getState(uiState)
|
||||
if (fieldState.textFieldValue.text.isEmpty()) {
|
||||
uiState = uiBuilder.checkSeedPhrase.updateTextFieldError(uiState, field, hasError = false)
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launchIo {
|
||||
val textFieldDebouncer = textFieldsDebouncers[field] ?: Debouncer().apply {
|
||||
textFieldsDebouncers[field] = this
|
||||
}
|
||||
textFieldDebouncer.debounce(viewModelScope, context = dispatchers.io) {
|
||||
val hasError = !seedPhraseInteractor.isPhraseMatch(
|
||||
phrase = textFieldValue.text,
|
||||
source = uiState.yourSeedPhraseState.phraseList,
|
||||
)
|
||||
if (fieldState.isError != hasError) {
|
||||
withMainContext {
|
||||
uiState = uiBuilder.checkSeedPhrase.updateTextFieldError(
|
||||
uiState = uiState,
|
||||
field = field,
|
||||
hasError = hasError,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val allFieldsWithoutError = SeedPhraseField.values()
|
||||
.map { it.getState(uiState) }
|
||||
.all { !it.isError }
|
||||
|
||||
if (uiState.checkSeedPhraseState.buttonCreateWallet.enabled != allFieldsWithoutError) {
|
||||
withMainContext {
|
||||
uiState = uiBuilder.checkSeedPhrase.updateCreateWalletButton(
|
||||
uiState = uiState,
|
||||
enabled = allFieldsWithoutError,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion CheckSeedPhrase
|
||||
|
||||
// region ButtonClickHandlers
|
||||
private fun buttonCreateWalletClick() {
|
||||
}
|
||||
|
||||
private fun buttonCreateWalletWithSeedPhraseClick() {
|
||||
buttonGenerateSeedPhraseClick()
|
||||
}
|
||||
|
||||
private fun buttonOtherOptionsClick() {
|
||||
uiState = uiBuilder.changeStep(uiState, OnboardingSeedPhraseStep.AboutSeedPhrase)
|
||||
}
|
||||
|
||||
private fun buttonReadMoreAboutSeedPhraseClick() {
|
||||
aboutSeedPhraseUriProvider?.open()
|
||||
}
|
||||
|
||||
private fun buttonGenerateSeedPhraseClick() {
|
||||
viewModelScope.launchIo {
|
||||
withMainContext {
|
||||
uiState = uiBuilder.generateSeedPhrase(uiState)
|
||||
}
|
||||
|
||||
delay(1000)
|
||||
val seedPhraseList = seedPhraseInteractor.generateSeedPhrase()
|
||||
withMainContext {
|
||||
uiState = uiBuilder.seedPhraseGenerated(uiState, seedPhraseList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buttonImportSeedPhraseClick() {
|
||||
uiState = uiBuilder.changeStep(uiState, OnboardingSeedPhraseStep.ImportSeedPhrase)
|
||||
}
|
||||
|
||||
private fun buttonContinueClick() {
|
||||
uiState = uiBuilder.changeStep(uiState, OnboardingSeedPhraseStep.CheckSeedPhrase)
|
||||
}
|
||||
|
||||
private fun buttonSuggestedPhraseClick(suggestionIndex: Int) {
|
||||
}
|
||||
|
||||
private fun menuChatClick() {
|
||||
chatLauncher?.open()
|
||||
}
|
||||
// endregion ButtonClickHandlers
|
||||
|
||||
// region Utils
|
||||
private fun SeedPhraseField.getState(uiState: OnboardingSeedPhraseState): TextFieldState = when (this) {
|
||||
SeedPhraseField.Second -> uiState.checkSeedPhraseState.tvSecondPhrase
|
||||
SeedPhraseField.Seventh -> uiState.checkSeedPhraseState.tvSeventhPhrase
|
||||
SeedPhraseField.Eleventh -> uiState.checkSeedPhraseState.tvEleventhPhrase
|
||||
}
|
||||
|
||||
private fun CoroutineScope.launchMain(block: suspend CoroutineScope.() -> Unit): Job {
|
||||
return viewModelScope.launch(dispatchers.main, block = block)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.launchIo(block: suspend CoroutineScope.() -> Unit): Job {
|
||||
return viewModelScope.launch(dispatchers.main, block = block)
|
||||
}
|
||||
|
||||
private suspend fun <T> withMainContext(block: suspend CoroutineScope.() -> T): T {
|
||||
return withContext(dispatchers.main, block)
|
||||
}
|
||||
|
||||
private suspend fun <T> withIoContext(block: suspend CoroutineScope.() -> T): T {
|
||||
return withContext(dispatchers.io, block)
|
||||
}
|
||||
// endregion Utils
|
||||
}
|
||||
|
||||
interface AboutSeedPhraseOpener {
|
||||
fun open() {}
|
||||
}
|
||||
|
||||
interface ChatSupportOpener {
|
||||
fun open() {}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue