From 78e0d5e3b3d5b072f06b68b8c6062709e226a144 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 3 Apr 2023 08:25:36 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../wallet2/ui/CheckSeedPhraseScreen.kt | 138 ++++++++++ .../wallet2/viewmodel/SeedPhraseViewModel.kt | 243 ++++++++++++++++++ 2 files changed, 381 insertions(+) create mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/CheckSeedPhraseScreen.kt create mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/CheckSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/CheckSeedPhraseScreen.kt new file mode 100644 index 0000000000..0f9b44759f --- /dev/null +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/CheckSeedPhraseScreen.kt @@ -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 +} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt new file mode 100644 index 0000000000..baa8f5aabf --- /dev/null +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt @@ -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() + + 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 withMainContext(block: suspend CoroutineScope.() -> T): T { + return withContext(dispatchers.main, block) + } + + private suspend fun withIoContext(block: suspend CoroutineScope.() -> T): T { + return withContext(dispatchers.io, block) + } + // endregion Utils +} + +interface AboutSeedPhraseOpener { + fun open() {} +} + +interface ChatSupportOpener { + fun open() {} +} \ No newline at end of file