Updated on 2026-08-14
This commit is contained in:
parent
f084f256b4
commit
6631ad696c
7 changed files with 223 additions and 3 deletions
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.utils.extensions
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
fun Char.isNotWhitespace(): Boolean = !this.isWhitespace()
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.utils.extensions
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
fun <T> Collection<T>.isSingleItem(): Boolean = this.size == 1
|
||||
|
|
@ -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,
|
||||
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ class StateBuilder(
|
|||
) {
|
||||
|
||||
val checkSeedPhrase: CheckSeedPhraseStateBuilder = CheckSeedPhraseStateBuilder()
|
||||
val importSeedPhrase: ImportSeedPhraseStateBuilder = ImportSeedPhraseStateBuilder()
|
||||
|
||||
fun init(): OnboardingSeedPhraseState = OnboardingSeedPhraseState(
|
||||
step = OnboardingSeedPhraseStep.Intro,
|
||||
|
|
|
|||
|
|
@ -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 = "_$@-*"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue