Updated on 2026-08-14

This commit is contained in:
Tangem 2024-03-28 19:56:01 +00:00
parent 4df89bb521
commit cf5a7d68ad
16 changed files with 406 additions and 66 deletions

View file

@ -1,6 +1,8 @@
package com.tangem.feature.onboarding.presentation.wallet2.model
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.feature.onboarding.domain.SeedPhraseError
import com.tangem.feature.onboarding.domain.models.MnemonicType
import kotlinx.collections.immutable.ImmutableList
@ -64,11 +66,6 @@ enum class SegmentSeedType(val count: Int) {
}
}
data class MnemonicState(
val seedType: SegmentSeedType,
val mnemonicItems: PersistentList<MnemonicGridItem>,
)
data class MnemonicGridItem(
val index: Int,
val mnemonic: String,
@ -82,14 +79,19 @@ data class CheckSeedPhraseState(
)
data class ImportSeedPhraseState(
val tvSeedPhrase: TextFieldState,
val fieldSeedPhrase: TextFieldState,
val fieldPassphrase: TextFieldState,
val onSuggestedPhraseClick: (Int) -> Unit,
val onPassphraseInfoClick: () -> Unit,
val buttonCreateWallet: ButtonState,
val invalidWords: Set<String> = emptySet(),
val suggestionsList: ImmutableList<String> = persistentListOf(),
val error: SeedPhraseError? = null,
val bottomSheetConfig: TangemBottomSheetConfig? = null,
)
data class ShowPassphraseInfoBottomSheetContent(val onOkClick: () -> Unit) : TangemBottomSheetConfigContent
data class TextFieldState(
val onTextFieldValueChanged: (TextFieldValue) -> Unit,
val textFieldValue: TextFieldValue = TextFieldValue(),

View file

@ -40,7 +40,9 @@ data class CheckSeedPhraseUiAction(
data class ImportSeedPhraseUiAction(
val phraseTextFieldAction: TextFieldUiAction,
val passTextFieldAction: TextFieldUiAction,
val suggestedPhraseClick: (Int) -> Unit,
val onPassphraseInfoClick: () -> Unit,
val buttonCreateWalletClick: () -> Unit,
)

View file

@ -1,13 +1,16 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@ -17,9 +20,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import com.tangem.core.ui.components.Notifier
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.TangemTextFieldsDefault
import com.tangem.core.ui.components.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.onboarding.R
import com.tangem.feature.onboarding.presentation.wallet2.model.ButtonState
@ -38,33 +39,31 @@ import kotlinx.collections.immutable.persistentListOf
*/
@Composable
fun ImportSeedPhraseScreen(state: ImportSeedPhraseState, modifier: Modifier = Modifier) {
Column(
val keyboard by keyboardAsState()
Box(
modifier = modifier.fillMaxSize(),
) {
Box(
modifier = Modifier.weight(1f),
) {
Column {
OnboardingDescriptionBlock(
modifier = Modifier.padding(top = TangemTheme.dimens.size16),
) {
DescriptionSubTitleText(text = stringResource(id = R.string.onboarding_seed_import_message))
}
PhraseBlock(
modifier = Modifier
.padding(top = TangemTheme.dimens.size62)
.padding(horizontal = TangemTheme.dimens.size16),
state = state,
)
SuggestionsBlock(
modifier = Modifier.padding(top = TangemTheme.dimens.size4),
suggestionsList = state.suggestionsList,
onClick = { index -> state.onSuggestedPhraseClick(index) },
)
Column {
OnboardingDescriptionBlock(
modifier = Modifier.padding(top = TangemTheme.dimens.size16),
) {
DescriptionSubTitleText(text = stringResource(id = R.string.onboarding_seed_import_message))
}
}
Box {
PhraseBlock(
modifier = Modifier
.padding(top = TangemTheme.dimens.size62)
.padding(horizontal = TangemTheme.dimens.size16),
state = state,
)
PassphraseBlock(
modifier = Modifier.padding(horizontal = TangemTheme.dimens.size16),
passphraseField = state.fieldPassphrase,
onPassphraseInfoClick = state.onPassphraseInfoClick,
)
OnboardingActionBlock(
modifier = Modifier.padding(top = TangemTheme.dimens.size16),
firstActionContent = {
PrimaryButtonIconEnd(
modifier = Modifier
@ -78,9 +77,41 @@ fun ImportSeedPhraseScreen(state: ImportSeedPhraseState, modifier: Modifier = Mo
},
)
}
if (keyboard is Keyboard.Opened) {
SuggestionsBlock(
modifier = Modifier
.align(Alignment.BottomCenter)
.imePadding(),
suggestionsList = state.suggestionsList,
onClick = { index -> state.onSuggestedPhraseClick(index) },
)
}
if (state.bottomSheetConfig != null) {
PassphraseInfoBottomSheet(config = state.bottomSheetConfig)
}
}
}
@Composable
private fun PassphraseBlock(
passphraseField: TextFieldState,
onPassphraseInfoClick: () -> Unit,
modifier: Modifier = Modifier,
) {
OutlineTextFieldWithIcon(
modifier = modifier.fillMaxWidth(),
value = passphraseField.textFieldValue,
onValueChange = passphraseField.onTextFieldValueChanged,
iconResId = R.drawable.ic_information_24,
iconColor = TangemTheme.colors.icon.informative,
label = stringResource(id = R.string.common_passphrase),
placeholder = stringResource(id = R.string.send_optional_field),
onIconClick = onPassphraseInfoClick,
)
}
@Composable
private fun PhraseBlock(state: ImportSeedPhraseState, modifier: Modifier = Modifier) {
// TODO: replace by TextReference
@ -93,8 +124,8 @@ private fun PhraseBlock(state: ImportSeedPhraseState, modifier: Modifier = Modif
modifier = Modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size142),
value = state.tvSeedPhrase.textFieldValue,
onValueChange = state.tvSeedPhrase.onTextFieldValueChanged,
value = state.fieldSeedPhrase.textFieldValue,
onValueChange = state.fieldSeedPhrase.onTextFieldValueChanged,
textStyle = TangemTheme.typography.body1,
singleLine = false,
visualTransformation = InvalidWordsColorTransformation(
@ -131,12 +162,12 @@ private fun SuggestionsBlock(
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
modifier = modifier,
enter = fadeIn() + slideIn(initialOffset = { IntOffset(x = 200, y = 0) }),
exit = slideOut(targetOffset = { IntOffset(x = -200, y = 0) }) + fadeOut(),
visible = suggestionsList.isNotEmpty(),
) {
LazyRow(
modifier = modifier,
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.size16),
) {
items(suggestionsList.size) { index ->
@ -160,7 +191,7 @@ private fun SuggestionButton(text: String, onClick: () -> Unit, modifier: Modifi
Box(modifier = modifier.clickable(onClick = onClick)) {
Notifier(
text = text,
backgroundColor = TangemTheme.colors.background.action,
backgroundColor = TangemTheme.colors.icon.primary1,
textColor = TangemTheme.colors.text.primary2,
)
}
@ -204,19 +235,23 @@ private fun ImportSeedPhraseScreenPreview_Light(
@PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList<String>,
) {
TangemTheme(isDark = false) {
ImportSeedPhraseScreen(
ImportSeedPhraseState(
tvSeedPhrase = TextFieldState(onTextFieldValueChanged = {}),
onSuggestedPhraseClick = {},
buttonCreateWallet = ButtonState(
enabled = true,
isClickable = true,
showProgress = false,
onClick = {},
Box(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
ImportSeedPhraseScreen(
ImportSeedPhraseState(
fieldSeedPhrase = TextFieldState(onTextFieldValueChanged = {}),
fieldPassphrase = TextFieldState(onTextFieldValueChanged = {}),
onSuggestedPhraseClick = {},
onPassphraseInfoClick = {},
buttonCreateWallet = ButtonState(
enabled = true,
isClickable = true,
showProgress = false,
onClick = {},
),
suggestionsList = suggestions,
),
suggestionsList = suggestions,
),
)
)
}
}
}

View file

@ -0,0 +1,89 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.onboarding.R
import com.tangem.feature.onboarding.presentation.wallet2.model.ShowPassphraseInfoBottomSheetContent
@Composable
fun PassphraseInfoBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet(
config = config,
containerColor = TangemTheme.colors.background.primary,
) { content: ShowPassphraseInfoBottomSheetContent ->
PassphraseInfoBottomSheetContent(content)
}
}
@Composable
fun PassphraseInfoBottomSheetContent(content: ShowPassphraseInfoBottomSheetContent) {
Column(
modifier = Modifier
.background(color = TangemTheme.colors.background.primary)
.fillMaxWidth(),
) {
Icon(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(top = TangemTheme.dimens.size40)
.size(TangemTheme.dimens.size48),
painter = painterResource(id = R.drawable.ic_information_24),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
Text(
text = stringResource(id = R.string.common_passphrase),
modifier = Modifier
.padding(top = TangemTheme.dimens.size40)
.align(Alignment.CenterHorizontally),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.h2,
)
Text(
text = stringResource(id = R.string.onboarding_bottom_sheet_passphrase_description),
modifier = Modifier
.padding(top = TangemTheme.dimens.size16)
.padding(horizontal = TangemTheme.dimens.size24)
.align(Alignment.CenterHorizontally),
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.body2,
textAlign = TextAlign.Center,
)
PrimaryButton(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.size16)
.padding(top = TangemTheme.dimens.size40)
.padding(bottom = TangemTheme.dimens.size32)
.fillMaxWidth(),
text = stringResource(id = R.string.common_ok),
onClick = content.onOkClick,
)
}
}
@Preview
@Composable
private fun PassphraseInfoBottomSheetContentPreview() {
TangemTheme {
PassphraseInfoBottomSheetContent(ShowPassphraseInfoBottomSheetContent { })
}
}

View file

@ -2,9 +2,11 @@ package com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.feature.onboarding.domain.InsertSuggestionResult
import com.tangem.feature.onboarding.domain.SeedPhraseError
import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState
import com.tangem.feature.onboarding.presentation.wallet2.model.ShowPassphraseInfoBottomSheetContent
import kotlinx.collections.immutable.ImmutableList
/**
@ -12,12 +14,23 @@ import kotlinx.collections.immutable.ImmutableList
*/
class ImportSeedPhraseStateBuilder {
fun updateTextField(
fun updateSeedPhraseTextField(
uiState: OnboardingSeedPhraseState,
textFieldValue: TextFieldValue,
): OnboardingSeedPhraseState = uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
tvSeedPhrase = uiState.importSeedPhraseState.tvSeedPhrase.copy(
fieldSeedPhrase = uiState.importSeedPhraseState.fieldSeedPhrase.copy(
textFieldValue = textFieldValue,
),
),
)
fun updatePassPhraseTextField(
uiState: OnboardingSeedPhraseState,
textFieldValue: TextFieldValue,
): OnboardingSeedPhraseState = uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
fieldPassphrase = uiState.importSeedPhraseState.fieldPassphrase.copy(
textFieldValue = textFieldValue,
),
),
@ -49,15 +62,40 @@ class ImportSeedPhraseStateBuilder {
),
)
fun showPassphraseInfoBottomSheet(
uiState: OnboardingSeedPhraseState,
onDismiss: () -> Unit,
): OnboardingSeedPhraseState {
return uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
bottomSheetConfig = TangemBottomSheetConfig(
isShow = true,
onDismissRequest = onDismiss,
content = ShowPassphraseInfoBottomSheetContent(onDismiss),
),
),
)
}
fun dismissPassphraseBottomSheet(uiState: OnboardingSeedPhraseState): OnboardingSeedPhraseState {
return uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
bottomSheetConfig = uiState.importSeedPhraseState.bottomSheetConfig?.copy(
isShow = false,
),
),
)
}
fun insertSuggestionWord(
uiState: OnboardingSeedPhraseState,
insertResult: InsertSuggestionResult,
): OnboardingSeedPhraseState {
val newTextFieldValue = uiState.importSeedPhraseState.tvSeedPhrase.textFieldValue.copy(
val newTextFieldValue = uiState.importSeedPhraseState.fieldSeedPhrase.textFieldValue.copy(
text = insertResult.text,
selection = TextRange(insertResult.cursorPosition, insertResult.cursorPosition),
)
return updateTextField(uiState, newTextFieldValue)
return updateSeedPhraseTextField(uiState, newTextFieldValue)
}
fun updateError(uiState: OnboardingSeedPhraseState, error: SeedPhraseError?): OnboardingSeedPhraseState {

View file

@ -28,11 +28,16 @@ class StateBuilder(
private fun getInitialImportSeedPhraseState(): ImportSeedPhraseState {
return ImportSeedPhraseState(
tvSeedPhrase = TextFieldState(
fieldSeedPhrase = TextFieldState(
onTextFieldValueChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onTextFieldChanged,
onFocusChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onFocusChanged,
),
fieldPassphrase = TextFieldState(
onTextFieldValueChanged = uiActions.importSeedPhraseActions.passTextFieldAction.onTextFieldChanged,
onFocusChanged = uiActions.importSeedPhraseActions.passTextFieldAction.onFocusChanged,
),
onSuggestedPhraseClick = uiActions.importSeedPhraseActions.suggestedPhraseClick,
onPassphraseInfoClick = uiActions.importSeedPhraseActions.onPassphraseInfoClick,
buttonCreateWallet = ButtonState(
enabled = false,
onClick = uiActions.importSeedPhraseActions.buttonCreateWalletClick,

View file

@ -11,6 +11,7 @@ interface SeedPhraseMediator {
fun createWallet(callback: (CompletionResult<CreateWalletResponse>) -> Unit)
fun importWallet(
mnemonicComponents: List<String>,
passphrase: String?,
seedPhraseSource: SeedPhraseSource,
callback: (CompletionResult<CreateWalletResponse>) -> Unit,
)

View file

@ -74,6 +74,7 @@ class SeedPhraseViewModel @Inject constructor(
private var generatedMnemonicComponents: Map<MnemonicType, List<String>> = emptyMap()
private var importedMnemonicComponents: List<String>? = null
private var importedPassphrase: String? = null
override fun onCleared() {
textFieldsDebouncers.forEach { entry -> entry.value.release() }
@ -143,7 +144,7 @@ class SeedPhraseViewModel @Inject constructor(
buttonCreateWalletClick = {
val mnemonicComponents =
generatedMnemonicComponents[selectedMnemonicType] ?: return@CheckSeedPhraseUiAction
buttonImportWalletClick(mnemonicComponents, SeedPhraseSource.GENERATED)
buttonImportWalletClick(mnemonicComponents, null, SeedPhraseSource.GENERATED)
},
secondTextFieldAction = TextFieldUiAction(
onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Second, value) },
@ -159,10 +160,14 @@ class SeedPhraseViewModel @Inject constructor(
phraseTextFieldAction = TextFieldUiAction(
onTextFieldChanged = { value -> onSeedPhraseTextFieldChanged(value) },
),
passTextFieldAction = TextFieldUiAction(
onTextFieldChanged = { value -> onPassPhraseTextFieldChanged(value) },
),
suggestedPhraseClick = ::buttonSuggestedPhraseClick,
onPassphraseInfoClick = ::buttonPassphraseInfoClick,
buttonCreateWalletClick = {
analyticsEventHandler.send(SeedPhraseEvents.ButtonImport)
buttonImportWalletClick(importedMnemonicComponents, SeedPhraseSource.IMPORTED)
buttonImportWalletClick(importedMnemonicComponents, importedPassphrase, SeedPhraseSource.IMPORTED)
},
),
menuChatClick = ::menuChatClick,
@ -208,13 +213,13 @@ class SeedPhraseViewModel @Inject constructor(
// region ImportSeedPhrase
private fun onSeedPhraseTextFieldChanged(textFieldValue: TextFieldValue) {
val oldTextFieldValue = uiState.importSeedPhraseState.tvSeedPhrase.textFieldValue
val oldTextFieldValue = uiState.importSeedPhraseState.fieldSeedPhrase.textFieldValue
val isSameText = textFieldValue.text == oldTextFieldValue.text
val isCursorMoved = textFieldValue.selection != oldTextFieldValue.selection
uiState = uiBuilder.importSeedPhrase.updateTextField(uiState, textFieldValue)
uiState = uiBuilder.importSeedPhrase.updateSeedPhraseTextField(uiState, textFieldValue)
val fieldState = uiState.importSeedPhraseState.tvSeedPhrase
val fieldState = uiState.importSeedPhraseState.fieldSeedPhrase
val inputMnemonic = fieldState.textFieldValue.text
val debouncer = createOrGetDebouncer(MNEMONIC_DEBOUNCER)
@ -236,6 +241,11 @@ class SeedPhraseViewModel @Inject constructor(
}
}
private fun onPassPhraseTextFieldChanged(textFieldValue: TextFieldValue) {
importedPassphrase = textFieldValue.text
uiState = uiBuilder.importSeedPhrase.updatePassPhraseTextField(uiState, textFieldValue)
}
private suspend fun validateMnemonic(inputMnemonic: String) {
if (inputMnemonic.isEmpty() && uiState.importSeedPhraseState.error != null) {
val mediateState = uiBuilder.importSeedPhrase.updateCreateWalletButton(uiState, enabled = false)
@ -284,11 +294,15 @@ class SeedPhraseViewModel @Inject constructor(
}
}
private fun buttonImportWalletClick(mnemonicComponents: List<String>?, seedPhraseSource: SeedPhraseSource) {
private fun buttonImportWalletClick(
mnemonicComponents: List<String>?,
passphrase: String?,
seedPhraseSource: SeedPhraseSource,
) {
mnemonicComponents ?: return
viewModelScope.launch(dispatchers.io) {
mediator.importWallet(mnemonicComponents, seedPhraseSource, ::handleWalletCreationResult)
mediator.importWallet(mnemonicComponents, passphrase, seedPhraseSource, ::handleWalletCreationResult)
}
}
@ -391,7 +405,7 @@ class SeedPhraseViewModel @Inject constructor(
private fun buttonSuggestedPhraseClick(suggestionIndex: Int) {
launchSingle {
val textFieldValue = uiState.importSeedPhraseState.tvSeedPhrase.textFieldValue
val textFieldValue = uiState.importSeedPhraseState.fieldSeedPhrase.textFieldValue
val word = uiState.importSeedPhraseState.suggestionsList[suggestionIndex]
val cursorPosition = textFieldValue.selection.end
@ -406,6 +420,22 @@ class SeedPhraseViewModel @Inject constructor(
}
}
}
private fun buttonPassphraseInfoClick() {
launchSingle {
updateUi {
uiBuilder.importSeedPhrase.showPassphraseInfoBottomSheet(uiState) {
dismissPassphraseBottomSheet()
}
}
}
}
private fun dismissPassphraseBottomSheet() {
launchSingle {
updateUi { uiBuilder.importSeedPhrase.dismissPassphraseBottomSheet(uiState) }
}
}
// endregion ButtonClickHandlers
// region Utils