Updated on 2026-08-14

This commit is contained in:
Tangem 2023-04-03 08:25:36 +03:00
parent ca35718e95
commit 78e0d5e3b3
2 changed files with 381 additions and 0 deletions

View file

@ -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, lets check",
subTitle = "To check wether youve 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
}

View file

@ -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() {}
}