Updated on 2026-08-14

This commit is contained in:
Tangem 2024-03-28 19:56:29 +00:00
parent 23db294dc5
commit 544d99b169
17 changed files with 369 additions and 177 deletions

View file

@ -313,7 +313,6 @@
<string name="onboarding_linking_error_card_with_wallets">Другой кошелек уже был создан на карте, которую вы пытаетесь добавить. Хотите сбросить его и использовать карту для бэкапа?</string>
<string name="onboarding_navbar_title_creating_backup">Резервная копия</string>
<string name="onboarding_seed_button_read_more">Прочитать о seed-фразе</string>
<string name="onboarding_seed_generate_message">Запишите эти 12 слов в порядке, указанном ниже, и сохраните их в надежном месте.</string>
<plurals name="onboarding_seed_generate_message_words_count">
<item quantity="one"></item>
<item quantity="few">Запишите эти %d слова в порядке, указанном ниже, и сохраните их в надежном месте.</item>

View file

@ -174,7 +174,6 @@
<string name="onboarding_linking_error_card_with_wallets">您要添加的卡上已經創建了另一個錢包。你想重置它並將卡用於新錢包嗎?</string>
<string name="onboarding_navbar_title_creating_backup">創建備份</string>
<string name="onboarding_seed_button_read_more">閱讀更多關於助記詞的訊息</string>
<string name="onboarding_seed_generate_message">按照下面給出的順序寫下這 12 個單詞,並將它們存放在安全秘密的地方。</string>
<plurals name="onboarding_seed_generate_message_words_count">
<item quantity="other">按照下面給出的順序寫下這 %d 個單詞,並將它們存放在安全秘密的地方。</item>
</plurals>

View file

@ -313,7 +313,6 @@
<string name="onboarding_linking_error_card_with_wallets">Another wallet has already been created on the card you\'re trying to add. Do you want to reset it and use the card for a new wallet?</string>
<string name="onboarding_navbar_title_creating_backup">Creating a backup</string>
<string name="onboarding_seed_button_read_more">Read more about seed phrase</string>
<string name="onboarding_seed_generate_message">Write these 12 words down in the order given below and store them in a safe and secret place.</string>
<plurals name="onboarding_seed_generate_message_words_count">
<item quantity="one"></item>
<item quantity="other">Write these %d words down in the order given below and store them in a safe and secret place.</item>

View file

@ -47,11 +47,15 @@ inline fun <reified T> SegmentedButtons(
selectedColor: Color = TangemTheme.colors.background.action,
dividerColor: Color = TangemTheme.colors.stroke.primary,
showIndication: Boolean = true,
selectedItem: T? = null,
crossinline buttonContent: @Composable (T) -> Unit,
) {
if (config.isEmpty() || config.size == 1) return
var selected by remember { mutableIntStateOf(0) }
var selected by remember {
val selectedIndex = if (selectedItem == null) 0 else config.indexOf(selectedItem)
mutableIntStateOf(selectedIndex)
}
Row(
modifier = modifier

View file

@ -125,6 +125,7 @@ data class TangemDimens internal constructor(
val spacing56: Dp = 56.dp,
val spacing60: Dp = 60.dp,
val spacing70: Dp = 70.dp,
val spacing76: Dp = 76.dp,
val spacing80: Dp = 80.dp,
val spacing92: Dp = 92.dp,
val spacing94: Dp = 94.dp,

View file

@ -4,6 +4,7 @@ import com.tangem.crypto.bip39.DefaultMnemonic
import com.tangem.crypto.bip39.EntropyLength
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.crypto.bip39.Wordlist
import com.tangem.feature.onboarding.domain.models.MnemonicType
/**
[REDACTED_AUTHOR]
@ -20,5 +21,12 @@ class DefaultMnemonicRepository(
return DefaultMnemonic(EntropyLength.Bits128Length, bip39Wordlist)
}
override fun generateMnemonic(mnemonicType: MnemonicType): Mnemonic {
return when (mnemonicType) {
MnemonicType.Mnemonic12 -> DefaultMnemonic(EntropyLength.Bits128Length, bip39Wordlist)
MnemonicType.Mnemonic24 -> DefaultMnemonic(EntropyLength.Bits256Length, bip39Wordlist)
}
}
override fun createMnemonic(mnemonicString: String): Mnemonic = DefaultMnemonic(mnemonicString, bip39Wordlist)
}

View file

@ -2,6 +2,7 @@ package com.tangem.feature.onboarding.data
import com.tangem.common.core.TangemSdkError
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.feature.onboarding.domain.models.MnemonicType
/**
[REDACTED_AUTHOR]
@ -13,6 +14,9 @@ interface MnemonicRepository {
@Throws(TangemSdkError.MnemonicException::class)
fun generateDefaultMnemonic(): Mnemonic
@Throws(TangemSdkError.MnemonicException::class)
fun generateMnemonic(mnemonicType: MnemonicType): Mnemonic
@Throws(TangemSdkError.MnemonicException::class)
fun createMnemonic(mnemonicString: String): Mnemonic
}

View file

@ -4,6 +4,7 @@ import com.tangem.common.core.TangemSdkError
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.crypto.bip39.MnemonicErrorResult
import com.tangem.feature.onboarding.data.MnemonicRepository
import com.tangem.feature.onboarding.domain.models.MnemonicType
import com.tangem.feature.onboarding.presentation.wallet2.model.SeedPhraseField
import com.tangem.utils.extensions.isNotWhitespace
import kotlinx.collections.immutable.ImmutableList
@ -13,27 +14,23 @@ import kotlinx.collections.immutable.toPersistentList
/**
[REDACTED_AUTHOR]
*/
internal class DefaultSeedPhraseInteractor constructor(
internal class DefaultSeedPhraseInteractor(
private val repository: MnemonicRepository,
) : SeedPhraseInteractor {
private var currentMnemonic: Mnemonic? = null
private val partWordFinder: PartWordFinder = PartWordFinder()
override suspend fun generateMnemonic(): Result<Mnemonic> {
return runCatching {
repository.generateDefaultMnemonic().apply {
currentMnemonic = this
}
repository.generateDefaultMnemonic()
}
}
override suspend fun getMnemonicComponents(): Result<List<String>> {
override suspend fun generateMnemonics(mnemonicTypes: List<MnemonicType>): Result<Map<MnemonicType, Mnemonic>> {
return runCatching {
val mnemonic = currentMnemonic ?: repository.generateDefaultMnemonic().apply {
currentMnemonic = this
mnemonicTypes.associateWith {
repository.generateMnemonic(it)
}
mnemonic.mnemonicComponents
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.feature.onboarding.domain
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.feature.onboarding.domain.models.MnemonicType
import com.tangem.feature.onboarding.presentation.wallet2.model.SeedPhraseField
import kotlinx.collections.immutable.ImmutableList
@ -9,7 +10,7 @@ import kotlinx.collections.immutable.ImmutableList
*/
interface SeedPhraseInteractor {
suspend fun generateMnemonic(): Result<Mnemonic>
suspend fun getMnemonicComponents(): Result<List<String>>
suspend fun generateMnemonics(mnemonicTypes: List<MnemonicType>): Result<Map<MnemonicType, Mnemonic>>
suspend fun isWordMatch(mnemonicComponents: List<String>?, field: SeedPhraseField, word: String): Boolean
suspend fun validateMnemonicString(text: String): Result<List<String>>
suspend fun getSuggestions(text: String, hasSelection: Boolean, cursorPosition: Int): ImmutableList<String>

View file

@ -0,0 +1,5 @@
package com.tangem.feature.onboarding.domain.models
enum class MnemonicType {
Mnemonic12, Mnemonic24;
}

View file

@ -2,7 +2,9 @@ package com.tangem.feature.onboarding.presentation.wallet2.model
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.feature.onboarding.domain.SeedPhraseError
import com.tangem.feature.onboarding.domain.models.MnemonicType
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
/**
@ -31,10 +33,42 @@ data class AboutState(
)
data class YourSeedPhraseState(
val mnemonicGridItems: ImmutableList<MnemonicGridItem> = persistentListOf(),
val segmentSeedState: SegmentSeedState,
val mnemonicGridItems: PersistentList<MnemonicGridItem> = persistentListOf(),
val buttonContinue: ButtonState,
)
data class SegmentSeedState(
val seedSegments: PersistentList<SegmentSeedType>,
val selectedSeedType: SegmentSeedType,
val onSelectType: (SegmentSeedType) -> Unit,
)
enum class SegmentSeedType(val count: Int) {
SEED_12(count = 12), SEED_24(count = 24);
fun toMnemonicType(): MnemonicType {
return when (this) {
SEED_12 -> MnemonicType.Mnemonic12
SEED_24 -> MnemonicType.Mnemonic24
}
}
companion object {
fun fromMnemonicType(mnemonicType: MnemonicType): SegmentSeedType {
return when (mnemonicType) {
MnemonicType.Mnemonic12 -> SEED_12
MnemonicType.Mnemonic24 -> SEED_24
}
}
}
}
data class MnemonicState(
val seedType: SegmentSeedType,
val mnemonicItems: PersistentList<MnemonicGridItem>,
)
data class MnemonicGridItem(
val index: Int,
val mnemonic: String,

View file

@ -28,6 +28,7 @@ data class AboutUiAction(
data class YourSeedPhraseUiAction(
val buttonContinueClick: () -> Unit,
val onSelectType: (SegmentSeedType) -> Unit,
)
data class CheckSeedPhraseUiAction(

View file

@ -2,93 +2,162 @@ package com.tangem.feature.onboarding.presentation.wallet2.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
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.pluralStringResource
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.SpacerW32
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.onboarding.R
import com.tangem.feature.onboarding.presentation.wallet2.model.ButtonState
import com.tangem.feature.onboarding.presentation.wallet2.model.MnemonicGridItem
import com.tangem.feature.onboarding.presentation.wallet2.model.YourSeedPhraseState
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description
import com.tangem.feature.onboarding.presentation.wallet2.model.*
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.DescriptionSubTitleText
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.DescriptionTitleText
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
/**
[REDACTED_AUTHOR]
*/
@Composable
fun YourSeedPhraseScreen(state: YourSeedPhraseState, modifier: Modifier = Modifier) {
Column(
Box(
modifier = modifier.fillMaxSize(),
) {
Box(
modifier = Modifier.weight(weight = 0.6f),
Column(
modifier = Modifier
.padding(bottom = TangemTheme.dimens.spacing92)
.verticalScroll(rememberScrollState())
.fillMaxSize(),
) {
OnboardingDescriptionBlock(
modifier = Modifier.align(Alignment.Center),
) {
Description(
titleRes = R.string.onboarding_seed_generate_title,
subTitleRes = R.string.onboarding_seed_generate_message,
)
}
}
Box(
modifier = Modifier.weight(weight = 1f),
) {
PhraseGreedBlock(state.mnemonicGridItems)
}
SegmentSeedBlock(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing20),
state = state.segmentSeedState,
)
Box {
OnboardingActionBlock(
firstActionContent = {
PrimaryButton(
modifier = Modifier
.fillMaxWidth(),
text = stringResource(id = R.string.common_continue),
onClick = state.buttonContinue.onClick,
)
},
TitleBlock(state.segmentSeedState)
SeedPhraseGridBlock(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing20)
.padding(horizontal = TangemTheme.dimens.size16),
mnemonicGridItems = state.mnemonicGridItems,
)
}
OnboardingActionBlock(
modifier = Modifier.align(Alignment.BottomCenter),
firstActionContent = {
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.common_continue),
onClick = state.buttonContinue.onClick,
)
},
)
}
}
@Composable
private fun TitleBlock(state: SegmentSeedState) {
Box(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.size36)
.padding(top = TangemTheme.dimens.spacing20)
.fillMaxWidth(),
) {
Column {
DescriptionTitleText(text = stringResource(id = R.string.onboarding_seed_generate_title))
SpacerH16()
DescriptionSubTitleText(
text = pluralStringResource(
id = R.plurals.onboarding_seed_generate_message_words_count,
count = state.selectedSeedType.count,
state.selectedSeedType.count,
),
)
}
}
}
@Composable
private fun PhraseGreedBlock(mnemonicGridItems: ImmutableList<MnemonicGridItem>) {
LazyVerticalGrid(
columns = GridCells.Fixed(count = 2),
private fun SegmentSeedBlock(state: SegmentSeedState, modifier: Modifier = Modifier) {
SegmentedButtons(
modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing76),
config = state.seedSegments,
selectedItem = state.selectedSeedType,
onClick = {
state.onSelectType(it)
},
) {
this.items(
count = mnemonicGridItems.size,
) { index ->
Row(
modifier = Modifier.padding(all = TangemTheme.dimens.size8),
verticalAlignment = Alignment.CenterVertically,
) {
val item = mnemonicGridItems[index]
SpacerW32()
Text(
modifier = Modifier.width(TangemTheme.dimens.size40),
text = "${item.index}.",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
Text(
text = item.mnemonic,
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary1,
)
Text(
text = pluralStringResource(
id = R.plurals.onboarding_seed_generate_words_count,
count = it.count,
it.count,
),
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
}
}
@Composable
private fun SeedPhraseGridBlock(mnemonicGridItems: ImmutableList<MnemonicGridItem>, modifier: Modifier = Modifier) {
VerticalGrid(
modifier = modifier,
items = mnemonicGridItems,
columns = 2,
) { item ->
Row(
modifier = Modifier.padding(all = TangemTheme.dimens.size8),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.width(TangemTheme.dimens.size40),
text = "${item.index}.",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
Text(
text = item.mnemonic,
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary1,
)
}
}
}
@Composable
private inline fun <T> VerticalGrid(
items: ImmutableList<T>,
columns: Int,
modifier: Modifier = Modifier,
crossinline content: @Composable (T) -> Unit,
) {
val columnLength = items.size / columns
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
for (index in 0 until columns) {
Column {
for (i in 0 until columnLength) {
val item = items[index * columnLength + i]
content(item)
}
}
}
}
@ -100,7 +169,15 @@ private fun YourSeedPhraseScreenPreview_Light() {
TangemTheme(isDark = false) {
YourSeedPhraseScreen(
state = YourSeedPhraseState(
mnemonicGridItems = (1..12).map { MnemonicGridItem(it, it.toString()) }.toImmutableList(),
segmentSeedState = SegmentSeedState(
seedSegments = persistentListOf(
SegmentSeedType.SEED_12,
SegmentSeedType.SEED_24,
),
selectedSeedType = SegmentSeedType.SEED_24,
onSelectType = {},
),
mnemonicGridItems = (1..24).map { MnemonicGridItem(it, it.toString()) }.toPersistentList(),
buttonContinue = ButtonState(
enabled = true,
isClickable = true,

View file

@ -14,13 +14,14 @@ import com.tangem.core.ui.res.TangemTheme
*/
@Composable
fun OnboardingActionBlock(
modifier: Modifier = Modifier,
firstActionContent: @Composable (() -> Unit)? = null,
secondActionContent: @Composable (() -> Unit)? = null,
) {
if (firstActionContent == null && secondActionContent == null) return
Box(
modifier = Modifier
modifier = modifier
.padding(horizontal = TangemTheme.dimens.size16)
.padding(
top = TangemTheme.dimens.size8,

View file

@ -11,10 +11,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.onboarding.presentation.wallet2.model.DescriptionResource
import kotlinx.collections.immutable.ImmutableList
/**
* UI component witch provides single and carousel description for the onboarding process
@ -24,7 +22,7 @@ fun OnboardingDescriptionBlock(modifier: Modifier = Modifier, content: @Composab
Box(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.size24),
.padding(horizontal = TangemTheme.dimens.size36),
) {
content()
}
@ -34,22 +32,11 @@ fun OnboardingDescriptionBlock(modifier: Modifier = Modifier, content: @Composab
fun Description(@StringRes titleRes: Int, @StringRes subTitleRes: Int) {
Column {
DescriptionTitleText(text = stringResource(id = titleRes))
SpacerH12()
SpacerH16()
DescriptionSubTitleText(text = stringResource(id = subTitleRes))
}
}
@Suppress("UnusedPrivateMember")
@Composable
fun OnboardingCarouselDescriptionBlock(
descriptionsList: ImmutableList<DescriptionResource>,
modifier: Modifier = Modifier,
) {
OnboardingDescriptionBlock(modifier) {
// TODO: implement
}
}
@Composable
fun DescriptionTitleText(text: String) {
Text(

View file

@ -1,7 +1,8 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders
import com.tangem.feature.onboarding.presentation.wallet2.model.*
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
/**
[REDACTED_AUTHOR]
@ -13,32 +14,34 @@ class StateBuilder(
val checkSeedPhrase: CheckSeedPhraseStateBuilder = CheckSeedPhraseStateBuilder()
val importSeedPhrase: ImportSeedPhraseStateBuilder = ImportSeedPhraseStateBuilder()
fun init(): OnboardingSeedPhraseState = OnboardingSeedPhraseState(
introState = IntroState(
fun init(selectedSeedType: SegmentSeedType): OnboardingSeedPhraseState = OnboardingSeedPhraseState(
introState = getInitialIntroState(),
aboutState = getInitialAboutState(),
yourSeedPhraseState = getInitialYourSeedPhraseState(selectedSeedType),
checkSeedPhraseState = getInitialCheckSeedPhraseState(),
importSeedPhraseState = getInitialImportSeedPhraseState(),
menuButtonChat = ButtonState(
onClick = uiActions.menuChatClick,
),
onBackClick = uiActions.menuNavigateBackClick,
)
private fun getInitialImportSeedPhraseState(): ImportSeedPhraseState {
return ImportSeedPhraseState(
tvSeedPhrase = TextFieldState(
onTextFieldValueChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onTextFieldChanged,
onFocusChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onFocusChanged,
),
onSuggestedPhraseClick = uiActions.importSeedPhraseActions.suggestedPhraseClick,
buttonCreateWallet = ButtonState(
onClick = uiActions.introActions.buttonCreateWalletClick,
enabled = false,
onClick = uiActions.importSeedPhraseActions.buttonCreateWalletClick,
),
buttonOtherOptions = ButtonState(
onClick = uiActions.introActions.buttonOtherOptionsClick,
),
),
aboutState = AboutState(
buttonReadMoreAboutSeedPhrase = ButtonState(
onClick = uiActions.aboutActions.buttonReadMoreAboutSeedPhraseClick,
),
buttonGenerateSeedPhrase = ButtonState(
onClick = uiActions.aboutActions.buttonGenerateSeedPhraseClick,
),
buttonImportSeedPhrase = ButtonState(
onClick = uiActions.aboutActions.buttonImportSeedPhraseClick,
),
),
yourSeedPhraseState = YourSeedPhraseState(
buttonContinue = ButtonState(
onClick = uiActions.yourSeedPhraseActions.buttonContinueClick,
),
),
checkSeedPhraseState = CheckSeedPhraseState(
)
}
private fun getInitialCheckSeedPhraseState(): CheckSeedPhraseState {
return CheckSeedPhraseState(
tvSecondPhrase = TextFieldState(
label = "2",
isFocused = true,
@ -61,23 +64,49 @@ class StateBuilder(
enabled = false,
onClick = uiActions.checkSeedPhraseActions.buttonCreateWalletClick,
),
),
importSeedPhraseState = ImportSeedPhraseState(
tvSeedPhrase = TextFieldState(
onTextFieldValueChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onTextFieldChanged,
onFocusChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onFocusChanged,
)
}
private fun getInitialYourSeedPhraseState(selectedSeedType: SegmentSeedType): YourSeedPhraseState {
return YourSeedPhraseState(
segmentSeedState = SegmentSeedState(
seedSegments = persistentListOf(
SegmentSeedType.SEED_12,
SegmentSeedType.SEED_24,
),
selectedSeedType = selectedSeedType,
onSelectType = uiActions.yourSeedPhraseActions.onSelectType,
),
onSuggestedPhraseClick = uiActions.importSeedPhraseActions.suggestedPhraseClick,
buttonContinue = ButtonState(
onClick = uiActions.yourSeedPhraseActions.buttonContinueClick,
),
)
}
private fun getInitialAboutState(): AboutState {
return AboutState(
buttonReadMoreAboutSeedPhrase = ButtonState(
onClick = uiActions.aboutActions.buttonReadMoreAboutSeedPhraseClick,
),
buttonGenerateSeedPhrase = ButtonState(
onClick = uiActions.aboutActions.buttonGenerateSeedPhraseClick,
),
buttonImportSeedPhrase = ButtonState(
onClick = uiActions.aboutActions.buttonImportSeedPhraseClick,
),
)
}
private fun getInitialIntroState(): IntroState {
return IntroState(
buttonCreateWallet = ButtonState(
enabled = false,
onClick = uiActions.importSeedPhraseActions.buttonCreateWalletClick,
onClick = uiActions.introActions.buttonCreateWalletClick,
),
),
menuButtonChat = ButtonState(
onClick = uiActions.menuChatClick,
),
onBackClick = uiActions.menuNavigateBackClick,
)
buttonOtherOptions = ButtonState(
onClick = uiActions.introActions.buttonOtherOptionsClick,
),
)
}
fun setCardArtwork(uiState: OnboardingSeedPhraseState, cardArtwork: String): OnboardingSeedPhraseState =
uiState.copy(
@ -101,7 +130,7 @@ class StateBuilder(
fun mnemonicGenerated(
uiState: OnboardingSeedPhraseState,
mnemonicGridItems: ImmutableList<MnemonicGridItem>,
mnemonicGridItems: PersistentList<MnemonicGridItem>,
): OnboardingSeedPhraseState {
return updateMnemonicComponents(uiState, mnemonicGridItems)
.copy(
@ -116,9 +145,45 @@ class StateBuilder(
)
}
fun updateMnemonicComponents(
fun selectSeedType(
uiState: OnboardingSeedPhraseState,
mnemonicGridItems: ImmutableList<MnemonicGridItem>,
mnemonicGridItems: PersistentList<MnemonicGridItem>,
selectedSeedType: SegmentSeedType,
): OnboardingSeedPhraseState = uiState.copy(
yourSeedPhraseState = uiState.yourSeedPhraseState.copy(
mnemonicGridItems = mnemonicGridItems,
segmentSeedState = uiState.yourSeedPhraseState.segmentSeedState.copy(
selectedSeedType = selectedSeedType,
),
),
)
fun clearCheckSeedPhraseState(uiState: OnboardingSeedPhraseState): OnboardingSeedPhraseState = uiState.copy(
checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
tvSecondPhrase = TextFieldState(
label = "2",
isFocused = true,
onTextFieldValueChanged = uiActions.checkSeedPhraseActions.secondTextFieldAction.onTextFieldChanged,
onFocusChanged = uiActions.checkSeedPhraseActions.secondTextFieldAction.onFocusChanged,
),
tvSeventhPhrase = TextFieldState(
label = "7",
isFocused = false,
onTextFieldValueChanged = uiActions.checkSeedPhraseActions.seventhTextFieldAction.onTextFieldChanged,
onFocusChanged = uiActions.checkSeedPhraseActions.seventhTextFieldAction.onFocusChanged,
),
tvEleventhPhrase = TextFieldState(
label = "11",
isFocused = false,
onTextFieldValueChanged = uiActions.checkSeedPhraseActions.eleventhTextFieldAction.onTextFieldChanged,
onFocusChanged = uiActions.checkSeedPhraseActions.eleventhTextFieldAction.onFocusChanged,
),
),
)
private fun updateMnemonicComponents(
uiState: OnboardingSeedPhraseState,
mnemonicGridItems: PersistentList<MnemonicGridItem>,
): OnboardingSeedPhraseState = uiState.copy(
yourSeedPhraseState = uiState.yourSeedPhraseState.copy(
mnemonicGridItems = mnemonicGridItems,

View file

@ -13,18 +13,19 @@ import com.tangem.data.common.locale.LocaleProvider
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.domain.SeedPhraseError
import com.tangem.feature.onboarding.domain.SeedPhraseInteractor
import com.tangem.feature.onboarding.domain.models.MnemonicType
import com.tangem.feature.onboarding.presentation.wallet2.analytics.CreateWalletEvents
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseEvents
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
import com.tangem.feature.onboarding.presentation.wallet2.model.*
import com.tangem.feature.onboarding.presentation.wallet2.model.SegmentSeedType.Companion.fromMnemonicType
import com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders.StateBuilder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.Debouncer
import com.tangem.utils.extensions.isEven
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@ -43,7 +44,8 @@ class SeedPhraseViewModel @Inject constructor(
private var uiBuilder = StateBuilder(uiActions = createUiActions())
var uiState: OnboardingSeedPhraseState by mutableStateOf(uiBuilder.init())
private var selectedMnemonicType = MnemonicType.Mnemonic12
var uiState: OnboardingSeedPhraseState by mutableStateOf(uiBuilder.init(fromMnemonicType(selectedMnemonicType)))
private set
private lateinit var router: SeedPhraseRouter
@ -70,7 +72,7 @@ class SeedPhraseViewModel @Inject constructor(
private val textFieldsDebouncers = mutableMapOf<String, Debouncer>()
private var generatedMnemonicComponents: List<String>? = null
private var generatedMnemonicComponents: Map<MnemonicType, List<String>> = emptyMap()
private var importedMnemonicComponents: List<String>? = null
override fun onCleared() {
@ -135,10 +137,13 @@ class SeedPhraseViewModel @Inject constructor(
),
yourSeedPhraseActions = YourSeedPhraseUiAction(
buttonContinueClick = ::buttonContinueClick,
onSelectType = ::onSelectType,
),
checkSeedPhraseActions = CheckSeedPhraseUiAction(
buttonCreateWalletClick = {
buttonImportWalletClick(generatedMnemonicComponents, SeedPhraseSource.GENERATED)
val mnemonicComponents =
generatedMnemonicComponents[selectedMnemonicType] ?: return@CheckSeedPhraseUiAction
buttonImportWalletClick(mnemonicComponents, SeedPhraseSource.GENERATED)
},
secondTextFieldAction = TextFieldUiAction(
onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Second, value) },
@ -179,11 +184,12 @@ class SeedPhraseViewModel @Inject constructor(
}
createOrGetDebouncer(field.name).debounce(viewModelScope, context = dispatchers.io) {
val hasError = !interactor.isWordMatch(generatedMnemonicComponents, field, textFieldValue.text)
val mnemonicComponents = generatedMnemonicComponents[selectedMnemonicType] ?: return@debounce
val hasError = !interactor.isWordMatch(mnemonicComponents, field, textFieldValue.text)
if (fieldState.isError != hasError) {
updateUi { uiBuilder.checkSeedPhrase.updateTextFieldError(uiState, field, hasError) }
}
val isCreateWalletButtonEnabled = SeedPhraseField.values()
val isCreateWalletButtonEnabled = SeedPhraseField.entries
.map { field -> field.getState(uiState) }
.all { fieldState -> fieldState.textFieldValue.text.isNotEmpty() && !fieldState.isError }
@ -324,50 +330,35 @@ class SeedPhraseViewModel @Inject constructor(
}
private fun buttonGenerateSeedPhraseClick() {
val mnemonicTypes = listOf(MnemonicType.Mnemonic12, MnemonicType.Mnemonic24)
analyticsEventHandler.send(SeedPhraseEvents.ButtonGenerateSeedPhrase)
launchSingle {
updateUi { uiBuilder.generateMnemonicComponents(uiState) }
delay(DELAY_GENERATE_SEED_PHRASE)
interactor.generateMnemonic()
.onSuccess { mnemonic ->
generatedMnemonicComponents = mnemonic.mnemonicComponents
val mnemonicGridItems = generateMnemonicGridList(mnemonic.mnemonicComponents)
interactor.generateMnemonics(mnemonicTypes)
.onSuccess { mnemonics ->
generatedMnemonicComponents = mnemonics.mapValues { it.value.mnemonicComponents }
val defaultMnemonic = mnemonics[selectedMnemonicType]?.mnemonicComponents ?: return@launchSingle
val mnemonicGridItems = toMnemonicGridList(defaultMnemonic)
updateUi {
router.openScreen(SeedPhraseScreen.YourSeedPhrase)
uiBuilder.mnemonicGenerated(uiState, mnemonicGridItems.toImmutableList())
uiBuilder.mnemonicGenerated(uiState, mnemonicGridItems.toPersistentList())
}
}
.onFailure {
generatedMnemonicComponents = null
generatedMnemonicComponents = emptyMap()
// TODO: show error
}
}
}
private fun generateMnemonicGridList(mnemonicComponents: List<String>): ImmutableList<MnemonicGridItem> {
val size = mnemonicComponents.size
val splitIndex = if (size.isEven()) size / 2 else size / 2 + 1
val leftColumn = mnemonicComponents.subList(0, splitIndex)
val rightColumn = mnemonicComponents.subList(splitIndex, size)
val mnemonicGridItems = mutableListOf<MnemonicGridItem>()
for (index in 0 until splitIndex) {
if (index <= leftColumn.size) {
val item = MnemonicGridItem(
index = index + 1,
mnemonic = leftColumn[index],
)
mnemonicGridItems.add(item)
}
if (index < rightColumn.size) {
val item = MnemonicGridItem(
index = index + splitIndex + 1,
mnemonic = rightColumn[index],
)
mnemonicGridItems.add(item)
}
}
return mnemonicGridItems.toImmutableList()
private fun toMnemonicGridList(mnemonicComponents: List<String>): PersistentList<MnemonicGridItem> {
return mnemonicComponents.mapIndexed { index, word ->
MnemonicGridItem(
index = index.inc(),
mnemonic = word,
)
}.toPersistentList()
}
private fun buttonImportSeedPhraseClick() {
@ -376,7 +367,26 @@ class SeedPhraseViewModel @Inject constructor(
}
private fun buttonContinueClick() {
router.openScreen(SeedPhraseScreen.CheckSeedPhrase)
launchSingle {
updateUi {
uiBuilder.clearCheckSeedPhraseState(uiState)
}
router.openScreen(SeedPhraseScreen.CheckSeedPhrase)
}
}
private fun onSelectType(selectedSeedType: SegmentSeedType) {
launchSingle {
selectedMnemonicType = selectedSeedType.toMnemonicType()
val selectedMnemonic = generatedMnemonicComponents[selectedMnemonicType] ?: return@launchSingle
updateUi {
uiBuilder.selectSeedType(
uiState = uiState,
mnemonicGridItems = toMnemonicGridList(selectedMnemonic),
selectedSeedType = selectedSeedType,
)
}
}
}
private fun buttonSuggestedPhraseClick(suggestionIndex: Int) {
@ -433,9 +443,9 @@ class SeedPhraseViewModel @Inject constructor(
}
// endregion Utils
companion object {
private const val MNEMONIC_DEBOUNCER = "MnemonicDebouncer"
private const val MNEMONIC_DEBOUNCE_DELAY = 700L
private const val DELAY_GENERATE_SEED_PHRASE = 300L
private companion object {
const val MNEMONIC_DEBOUNCER = "MnemonicDebouncer"
const val MNEMONIC_DEBOUNCE_DELAY = 700L
const val DELAY_GENERATE_SEED_PHRASE = 300L
}
}