From 6631ad696c0b5d417ac06fe19bb50bb6c28bf052 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 16 Apr 2023 21:14:54 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../java/com/tangem/utils/extensions/Char.kt | 6 ++ .../com/tangem/utils/extensions/Collection.kt | 6 ++ .../wallet2/ui/ImportSeedPhraseScreen.kt | 10 ++- .../ui/ImportSeedPhraseStateBuilder.kt | 55 +++++++++++++ .../presentation/wallet2/ui/StateBuilder.kt | 1 + .../InvalidWordsColorTransformation.kt | 79 +++++++++++++++++++ .../wallet2/viewmodel/SeedPhraseViewModel.kt | 69 +++++++++++++++- 7 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 core/utils/src/main/java/com/tangem/utils/extensions/Char.kt create mode 100644 core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt create mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseStateBuilder.kt create mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/InvalidWordsColorTransformation.kt diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/Char.kt b/core/utils/src/main/java/com/tangem/utils/extensions/Char.kt new file mode 100644 index 0000000000..d4c95154b3 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/extensions/Char.kt @@ -0,0 +1,6 @@ +package com.tangem.utils.extensions + +/** +[REDACTED_AUTHOR] + */ +fun Char.isNotWhitespace(): Boolean = !this.isWhitespace() diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt b/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt new file mode 100644 index 0000000000..89fd92851b --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt @@ -0,0 +1,6 @@ +package com.tangem.utils.extensions + +/** +[REDACTED_AUTHOR] + */ +fun Collection.isSingleItem(): Boolean = this.size == 1 \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt index d79aa76288..f47740fc81 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.unit.IntOffset import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButtonIconLeft @@ -29,6 +30,7 @@ import com.tangem.feature.onboarding.presentation.wallet2.model.ImportSeedPhrase import com.tangem.feature.onboarding.presentation.wallet2.ui.components.DescriptionSubTitleText import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock +import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.InvalidWordsColorTransformation /** [REDACTED_AUTHOR] @@ -45,7 +47,9 @@ fun ImportSeedPhraseScreen( modifier = Modifier.weight(1f), ) { Column { - OnboardingDescriptionBlock(modifier) { + OnboardingDescriptionBlock( + modifier = Modifier.padding(top = TangemTheme.dimens.size16), + ) { DescriptionSubTitleText(text = stringResource(id = R.string.onboarding_seed_import_message)) } PhraseBlock( @@ -94,6 +98,10 @@ private fun PhraseBlock( onValueChange = state.tvSeedPhrase.onTextFieldValueChanged, textStyle = TangemTheme.typography.body1, singleLine = false, + visualTransformation = InvalidWordsColorTransformation( + wordsToBrush = state.invalidWords, + style = SpanStyle(color = TangemTheme.colors.text.warning), + ), colors = TangemTextFieldsDefault.defaultTextFieldColors, ) diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseStateBuilder.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseStateBuilder.kt new file mode 100644 index 0000000000..563152917c --- /dev/null +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseStateBuilder.kt @@ -0,0 +1,55 @@ +package com.tangem.feature.onboarding.presentation.wallet2.ui + +import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.feature.onboarding.domain.SeedPhraseError +import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState + +/** +[REDACTED_AUTHOR] + */ +class ImportSeedPhraseStateBuilder { + + fun updateTextField( + uiState: OnboardingSeedPhraseState, + textFieldValue: TextFieldValue, + ): OnboardingSeedPhraseState = uiState.copy( + importSeedPhraseState = uiState.importSeedPhraseState.copy( + tvSeedPhrase = uiState.importSeedPhraseState.tvSeedPhrase.copy( + textFieldValue = textFieldValue, + ), + ), + ) + + fun updateCreateWalletButton( + uiState: OnboardingSeedPhraseState, + enabled: Boolean, + ): OnboardingSeedPhraseState { + return uiState.copy( + importSeedPhraseState = uiState.importSeedPhraseState.copy( + buttonCreateWallet = uiState.importSeedPhraseState.buttonCreateWallet.copy( + enabled = enabled, + ), + ), + ) + } + + fun updateInvalidWords( + uiState: OnboardingSeedPhraseState, + invalidWords: Set, + ): OnboardingSeedPhraseState = uiState.copy( + importSeedPhraseState = uiState.importSeedPhraseState.copy( + invalidWords = invalidWords, + ), + ) + + fun updateError( + uiState: OnboardingSeedPhraseState, + error: SeedPhraseError?, + ): OnboardingSeedPhraseState { + return uiState.copy( + importSeedPhraseState = uiState.importSeedPhraseState.copy( + error = error, + ), + ) + } +} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/StateBuilder.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/StateBuilder.kt index 1a8a30975e..ddd2e242d3 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/StateBuilder.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/StateBuilder.kt @@ -21,6 +21,7 @@ class StateBuilder( ) { val checkSeedPhrase: CheckSeedPhraseStateBuilder = CheckSeedPhraseStateBuilder() + val importSeedPhrase: ImportSeedPhraseStateBuilder = ImportSeedPhraseStateBuilder() fun init(): OnboardingSeedPhraseState = OnboardingSeedPhraseState( step = OnboardingSeedPhraseStep.Intro, diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/InvalidWordsColorTransformation.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/InvalidWordsColorTransformation.kt new file mode 100644 index 0000000000..ec566f8013 --- /dev/null +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/InvalidWordsColorTransformation.kt @@ -0,0 +1,79 @@ +package com.tangem.feature.onboarding.presentation.wallet2.viewmodel + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation +import com.tangem.utils.extensions.isSingleItem + +/** +[REDACTED_AUTHOR] + */ +class InvalidWordsColorTransformation( + private val wordsToBrush: Set, + private val style: SpanStyle, +) : VisualTransformation { + + override fun filter(text: AnnotatedString): TransformedText { + val plainText = text.text + if (wordsToBrush.isSingleItem() && wordsToBrush.first() == plainText) { + return plainText.annotate().toTransformedText() + } + + val wordList = makeSeparatedWordlist(plainText) + val annotatedString = makeAnnotatedString(wordList) + + return annotatedString.toTransformedText() + } + + private fun makeSeparatedWordlist(plainText: String): List { + val plainTextCharArray = plainText.toCharArray() + val wordlist = mutableListOf() + var wordBuilder = StringBuilder() + + for (index in plainTextCharArray.indices) { + val char = plainTextCharArray[index] + if (char.isWhitespace()) { + if (wordBuilder.isEmpty()) { + wordlist.add(WORD_SEPARATOR) + } else { + wordlist.add(wordBuilder.toString()) + wordBuilder = StringBuilder() + wordlist.add(WORD_SEPARATOR) + } + } else { + if (index == plainTextCharArray.lastIndex) { + wordBuilder.append(char) + wordlist.add(wordBuilder.toString()) + wordBuilder = StringBuilder() + } else { + wordBuilder.append(char) + } + } + } + return wordlist.toList() + } + + private fun makeAnnotatedString(wordList: List): AnnotatedString = buildAnnotatedString { + wordList.forEach { + if (it == WORD_SEPARATOR) { + append(WHITE_SPACE) + } else if (wordsToBrush.contains(it)) { + append(it.annotate()) + } else { + append(it) + } + } + } + + private fun String.annotate(): AnnotatedString = AnnotatedString(this, style) + + private fun AnnotatedString.toTransformedText(): TransformedText = TransformedText(this, OffsetMapping.Identity) + + companion object { + private const val WHITE_SPACE = " " + private const val WORD_SEPARATOR = "_$@-*" + } +} 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 index 77db2b3f13..473781349e 100644 --- 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 @@ -6,6 +6,7 @@ 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.SeedPhraseError 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 @@ -84,8 +85,7 @@ class SeedPhraseViewModel @Inject constructor( ), importSeedPhraseActions = ImportSeedPhraseUiAction( phraseTextFieldAction = TextFieldUiAction( - onFocusChanged = { isFocused -> /* [REDACTED_TODO_COMMENT] */ }, - onTextFieldChanged = { value -> /* [REDACTED_TODO_COMMENT] */ }, + onTextFieldChanged = { value -> onSeedPhraseTextFieldChanged(value) }, ), suggestedPhraseClick = ::buttonSuggestedPhraseClick, buttonCreateWalletClick = ::buttonCreateWalletWithSeedPhraseClick, @@ -122,6 +122,66 @@ class SeedPhraseViewModel @Inject constructor( } // endregion CheckSeedPhrase + // region ImportSeedPhrase + private fun onSeedPhraseTextFieldChanged(textFieldValue: TextFieldValue) { + viewModelScope.launchSingle { + val oldTextFieldValue = uiState.importSeedPhraseState.tvSeedPhrase.textFieldValue + val isSameText = textFieldValue.text == oldTextFieldValue.text + val isCursorMoved = textFieldValue.selection != oldTextFieldValue.selection + + updateUi { + val mediateState = uiBuilder.importSeedPhrase.updateTextField(uiState, textFieldValue) + uiBuilder.importSeedPhrase.updateCreateWalletButton(mediateState, enabled = false) + } + + val fieldState = uiState.importSeedPhraseState.tvSeedPhrase + val inputMnemonic = fieldState.textFieldValue.text + + when { + isSameText && !isCursorMoved -> { + return@launchSingle + } + isSameText && isCursorMoved -> { + return@launchSingle + } + } + + val isPasteFromClipboard = textFieldValue.text.length - oldTextFieldValue.text.length > 1 + if (isPasteFromClipboard) { + validateMnemonic(inputMnemonic) + } else { + val debouncer = createOrGetDebouncer(MNEMONIC_DEBOUNCER) + debouncer.debounce(viewModelScope, MNEMONIC_DEBOUNCE_DELAY, dispatchers.io) { + validateMnemonic(inputMnemonic) + } + } + } + } + + private suspend fun validateMnemonic(inputMnemonic: String) { + if (inputMnemonic.isEmpty() && uiState.importSeedPhraseState.error != null) { + updateUi { uiBuilder.importSeedPhrase.updateError(uiState, null) } + return + } + + interactor.validateMnemonicString(inputMnemonic) + .onSuccess { + updateUi { uiBuilder.importSeedPhrase.updateError(uiState, null) } + } + .onFailure { + val error = it as? SeedPhraseError ?: return + + val mediateState = when (error) { + is SeedPhraseError.InvalidWords -> { + uiBuilder.importSeedPhrase.updateInvalidWords(uiState, error.words) + } + else -> uiState + } + updateUi { uiBuilder.importSeedPhrase.updateError(mediateState, error) } + } + } + // endregion ImportSeedPhrase + // region ButtonClickHandlers private fun buttonCreateWalletClick() { } @@ -231,4 +291,9 @@ class SeedPhraseViewModel @Inject constructor( return withContext(dispatchers.single, block) } // endregion Utils + + companion object { + private const val MNEMONIC_DEBOUNCER = "MnemonicDebouncer" + private const val MNEMONIC_DEBOUNCE_DELAY = 700L + } } \ No newline at end of file