Updated on 2026-08-14

This commit is contained in:
Tangem 2023-04-16 21:14:54 +03:00
parent f084f256b4
commit 6631ad696c
7 changed files with 223 additions and 3 deletions

View file

@ -0,0 +1,6 @@
package com.tangem.utils.extensions
/**
[REDACTED_AUTHOR]
*/
fun Char.isNotWhitespace(): Boolean = !this.isWhitespace()

View file

@ -0,0 +1,6 @@
package com.tangem.utils.extensions
/**
[REDACTED_AUTHOR]
*/
fun <T> Collection<T>.isSingleItem(): Boolean = this.size == 1

View file

@ -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,
)

View file

@ -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<String>,
): 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,
),
)
}
}

View file

@ -21,6 +21,7 @@ class StateBuilder(
) {
val checkSeedPhrase: CheckSeedPhraseStateBuilder = CheckSeedPhraseStateBuilder()
val importSeedPhrase: ImportSeedPhraseStateBuilder = ImportSeedPhraseStateBuilder()
fun init(): OnboardingSeedPhraseState = OnboardingSeedPhraseState(
step = OnboardingSeedPhraseStep.Intro,

View file

@ -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<String>,
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<String> {
val plainTextCharArray = plainText.toCharArray()
val wordlist = mutableListOf<String>()
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<String>): 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 = "_$@-*"
}
}

View file

@ -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
}
}