Updated on 2026-08-14
This commit is contained in:
parent
23db294dc5
commit
544d99b169
17 changed files with 369 additions and 177 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.feature.onboarding.domain.models
|
||||
|
||||
enum class MnemonicType {
|
||||
Mnemonic12, Mnemonic24;
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ data class AboutUiAction(
|
|||
|
||||
data class YourSeedPhraseUiAction(
|
||||
val buttonContinueClick: () -> Unit,
|
||||
val onSelectType: (SegmentSeedType) -> Unit,
|
||||
)
|
||||
|
||||
data class CheckSeedPhraseUiAction(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue