Updated on 2026-08-14
This commit is contained in:
parent
e6c8647f03
commit
6cc6bcfa4c
10 changed files with 327 additions and 62 deletions
|
|
@ -23,6 +23,7 @@ dependencies {
|
|||
implementation(projects.core.ui)
|
||||
implementation(projects.core.res)
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.navigation)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.models)
|
||||
|
|
@ -51,6 +52,7 @@ dependencies {
|
|||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.coil)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
implementation(deps.androidx.activity.compose)
|
||||
|
||||
/** Other libraries */
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -36,6 +37,10 @@ class MultiWalletSeedPhraseComponent(
|
|||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
BackHandler(enabled = true) {
|
||||
model.onBack()
|
||||
}
|
||||
|
||||
MultiWalletSeedPhrase(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model
|
||||
|
||||
import android.content.Context
|
||||
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.sdk.extensions.getWordlist
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class MnemonicRepository @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
private val wordlist = Wordlist.getWordlist(context)
|
||||
|
||||
val words: Set<String> = wordlist.words.toHashSet()
|
||||
|
||||
fun generateMnemonic(type: MnemonicType = MnemonicType.Words12): Mnemonic = DefaultMnemonic(
|
||||
entropy = when (type) {
|
||||
MnemonicType.Words12 -> EntropyLength.Bits128Length
|
||||
MnemonicType.Words24 -> EntropyLength.Bits256Length
|
||||
},
|
||||
wordlist = wordlist,
|
||||
)
|
||||
|
||||
fun generateMnemonic(mnemonicString: String): Mnemonic = DefaultMnemonic(
|
||||
mnemonic = mnemonicString,
|
||||
wordlist = wordlist,
|
||||
)
|
||||
|
||||
enum class MnemonicType {
|
||||
Words12, Words24,
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,11 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.mode
|
|||
|
||||
import com.tangem.core.decompose.di.ComponentScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder.GenerateSeedPhraseUiStateBuilder
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder.SeedPhraseCheckUiStateBuilder
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
|
@ -13,44 +15,92 @@ import javax.inject.Inject
|
|||
@ComponentScoped
|
||||
class MultiWalletSeedPhraseModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val mnemonicRepository: MnemonicRepository,
|
||||
private val urlOpener: UrlOpener,
|
||||
) : Model() {
|
||||
|
||||
private val state = MutableStateFlow(SeedPhraseState())
|
||||
private val _uiState = MutableStateFlow(getStartState())
|
||||
|
||||
private val _uiState = MutableStateFlow<MultiWalletSeedPhraseUM>(
|
||||
MultiWalletSeedPhraseUM.Start(
|
||||
onImportSeedPhraseClicked = {},
|
||||
onGenerateSeedPhraseClicked = {
|
||||
openGeneratedSeedPhrase()
|
||||
},
|
||||
onLearnMoreClicked = {},
|
||||
),
|
||||
private val generateSeedPhraseUiStateBuilder = GenerateSeedPhraseUiStateBuilder(
|
||||
state = state,
|
||||
updateUiState = { block -> updateUiStateSpecific(block) },
|
||||
onContinue = { openWordsCheck() },
|
||||
)
|
||||
|
||||
private val seedPhraseCheckUiStateBuilder = SeedPhraseCheckUiStateBuilder(
|
||||
state = state,
|
||||
currentUiState = { _uiState.value },
|
||||
updateUiState = { block -> updateUiStateSpecific(block) },
|
||||
importWallet = { },
|
||||
)
|
||||
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
fun onBack() {
|
||||
TODO("Not yet implemented")
|
||||
when (_uiState.value) {
|
||||
is MultiWalletSeedPhraseUM.Start -> {
|
||||
// TODO navigate back
|
||||
}
|
||||
is MultiWalletSeedPhraseUM.Import,
|
||||
is MultiWalletSeedPhraseUM.GenerateSeedPhrase,
|
||||
-> {
|
||||
_uiState.value = getStartState()
|
||||
state.value = SeedPhraseState()
|
||||
}
|
||||
is MultiWalletSeedPhraseUM.GeneratedWordsCheck -> {
|
||||
openGeneratedSeedPhrase()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openGeneratedSeedPhrase() {
|
||||
// TODO state.value.generatedWords
|
||||
val words = listOf("word, word")
|
||||
|
||||
_uiState.value = MultiWalletSeedPhraseUM.GenerateSeedPhrase(
|
||||
words24OptionSelected = false,
|
||||
words = words.mapIndexed { index, s ->
|
||||
MultiWalletSeedPhraseUM.GenerateSeedPhrase.MnemonicGridItem(index, s)
|
||||
}.toImmutableList(),
|
||||
onWords24OptionSwitch = {
|
||||
_uiState.update {
|
||||
if (it !is MultiWalletSeedPhraseUM.GenerateSeedPhrase) return@update it
|
||||
it.copy(words24OptionSelected = it.words24OptionSelected.not())
|
||||
}
|
||||
state.update { it.copy(words24Option = it.words24Option.not()) }
|
||||
private fun getStartState(): MultiWalletSeedPhraseUM {
|
||||
return MultiWalletSeedPhraseUM.Start(
|
||||
onImportSeedPhraseClicked = {},
|
||||
onGenerateSeedPhraseClicked = {
|
||||
generateSeedPhrase()
|
||||
openGeneratedSeedPhrase()
|
||||
},
|
||||
onContinueClick = {
|
||||
onLearnMoreClicked = {
|
||||
urlOpener.openUrl(seedPhraseLearnMoreUrl())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun openGeneratedSeedPhrase() {
|
||||
// TODO analytics SeedPhraseEvents.ButtonGenerateSeedPhrase
|
||||
|
||||
val words12 = state.value.generatedWords12 ?: return
|
||||
val words24 = state.value.generatedWords24 ?: return
|
||||
|
||||
// TODO disallow screenshots
|
||||
|
||||
_uiState.value = generateSeedPhraseUiStateBuilder.getState(
|
||||
generatedWords12 = words12,
|
||||
generatedWords24 = words24,
|
||||
)
|
||||
}
|
||||
|
||||
private fun openWordsCheck() {
|
||||
_uiState.value = seedPhraseCheckUiStateBuilder.getState()
|
||||
}
|
||||
|
||||
private fun generateSeedPhrase() {
|
||||
val words12 = mnemonicRepository.generateMnemonic(MnemonicRepository.MnemonicType.Words12)
|
||||
val words24 = mnemonicRepository.generateMnemonic(MnemonicRepository.MnemonicType.Words24)
|
||||
|
||||
state.update {
|
||||
it.copy(
|
||||
generatedWords12 = words12,
|
||||
generatedWords24 = words24,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T : MultiWalletSeedPhraseUM> updateUiStateSpecific(block: (T) -> T) {
|
||||
_uiState.update {
|
||||
if (it !is T) return@update it
|
||||
block(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model
|
||||
|
||||
import com.tangem.crypto.bip39.Mnemonic
|
||||
|
||||
data class SeedPhraseState(
|
||||
val generatedWords: List<String>? = null,
|
||||
val generatedWords12: Mnemonic? = null,
|
||||
val generatedWords24: Mnemonic? = null,
|
||||
val words24Option: Boolean = false,
|
||||
val readyToImport: Boolean = false,
|
||||
)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model
|
||||
|
||||
import android.net.Uri
|
||||
import com.tangem.utils.SupportedLanguages
|
||||
import java.util.Locale
|
||||
|
||||
internal fun seedPhraseLearnMoreUrl(): String {
|
||||
val language = Locale.getDefault().language
|
||||
|
||||
val languageBy = "by"
|
||||
if (SupportedLanguages.RUSSIAN.equals(language, true) ||
|
||||
languageBy.equals(language, true)
|
||||
) {
|
||||
SupportedLanguages.RUSSIAN
|
||||
} else {
|
||||
SupportedLanguages.ENGLISH
|
||||
}
|
||||
|
||||
val webUri = Uri.Builder()
|
||||
.scheme("https")
|
||||
.authority("tangem.com")
|
||||
.appendPath(language)
|
||||
.appendPath("blog/post/seed-phrase-a-risky-solution")
|
||||
.build()
|
||||
|
||||
return webUri.toString()
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder
|
||||
|
||||
import com.tangem.crypto.bip39.Mnemonic
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.SeedPhraseState
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class GenerateSeedPhraseUiStateBuilder(
|
||||
private val state: MutableStateFlow<SeedPhraseState>,
|
||||
private val updateUiState: (
|
||||
(MultiWalletSeedPhraseUM.GenerateSeedPhrase) -> MultiWalletSeedPhraseUM.GenerateSeedPhrase,
|
||||
) -> Unit,
|
||||
private val onContinue: () -> Unit,
|
||||
) {
|
||||
|
||||
fun getState(generatedWords12: Mnemonic, generatedWords24: Mnemonic): MultiWalletSeedPhraseUM.GenerateSeedPhrase {
|
||||
val words12 = generatedWords12.mnemonicComponents.mapIndexed { index, s ->
|
||||
MultiWalletSeedPhraseUM.GenerateSeedPhrase.MnemonicGridItem(index + 1, s)
|
||||
}.toImmutableList()
|
||||
val words24 = generatedWords24.mnemonicComponents.mapIndexed { index, s ->
|
||||
MultiWalletSeedPhraseUM.GenerateSeedPhrase.MnemonicGridItem(index + 1, s)
|
||||
}.toImmutableList()
|
||||
|
||||
return MultiWalletSeedPhraseUM.GenerateSeedPhrase(
|
||||
words24OptionSelected = state.value.words24Option,
|
||||
words = words12,
|
||||
onWords24OptionSwitch = { switchType(words12, words24) },
|
||||
onContinueClick = onContinue,
|
||||
)
|
||||
}
|
||||
|
||||
private fun switchType(
|
||||
generatedWords12: ImmutableList<MultiWalletSeedPhraseUM.GenerateSeedPhrase.MnemonicGridItem>,
|
||||
generatedWords24: ImmutableList<MultiWalletSeedPhraseUM.GenerateSeedPhrase.MnemonicGridItem>,
|
||||
) {
|
||||
updateUiState { uiSt ->
|
||||
state.update { it.copy(words24Option = uiSt.words24OptionSelected.not()) }
|
||||
|
||||
uiSt.copy(
|
||||
words24OptionSelected = uiSt.words24OptionSelected.not(),
|
||||
words = if (uiSt.words24OptionSelected) {
|
||||
generatedWords12
|
||||
} else {
|
||||
generatedWords24
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder
|
||||
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.SeedPhraseState
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class SeedPhraseCheckUiStateBuilder(
|
||||
private val state: MutableStateFlow<SeedPhraseState>,
|
||||
private val currentUiState: () -> MultiWalletSeedPhraseUM,
|
||||
private val updateUiState: (
|
||||
(MultiWalletSeedPhraseUM.GeneratedWordsCheck) -> MultiWalletSeedPhraseUM.GeneratedWordsCheck,
|
||||
) -> Unit,
|
||||
private val importWallet: () -> Unit,
|
||||
) {
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun getState(): MultiWalletSeedPhraseUM.GeneratedWordsCheck {
|
||||
val wordFields = List(3) { index ->
|
||||
val shownIndex = when (index) {
|
||||
0 -> 2
|
||||
1 -> 7
|
||||
2 -> 11
|
||||
else -> error("")
|
||||
}
|
||||
|
||||
MultiWalletSeedPhraseUM.GeneratedWordsCheck.WordField(
|
||||
index = shownIndex,
|
||||
word = TextFieldValue(""),
|
||||
error = false,
|
||||
onChange = { changeField ->
|
||||
updateUiState { st ->
|
||||
val newState = st.copy(
|
||||
wordFields = st.wordFields.map { wordField ->
|
||||
if (wordField.index == shownIndex) {
|
||||
wordField.updateField(changeField, shownIndex)
|
||||
} else {
|
||||
wordField
|
||||
}
|
||||
}.toImmutableList(),
|
||||
)
|
||||
|
||||
val allFieldsCorrect = newState.allFieldsCorrect()
|
||||
state.update { it.copy(readyToImport = allFieldsCorrect) }
|
||||
|
||||
newState.copy(
|
||||
createWalletButtonEnabled = allFieldsCorrect,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}.toImmutableList()
|
||||
|
||||
return MultiWalletSeedPhraseUM.GeneratedWordsCheck(
|
||||
wordFields = wordFields,
|
||||
createWalletButtonEnabled = false,
|
||||
createWalletButtonProgress = false,
|
||||
onCreateWalletButtonClick = {
|
||||
val currentState = currentUiState() as? MultiWalletSeedPhraseUM.GeneratedWordsCheck
|
||||
?: return@GeneratedWordsCheck
|
||||
if (currentState.createWalletButtonEnabled) {
|
||||
importWallet()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun MultiWalletSeedPhraseUM.GeneratedWordsCheck.WordField.updateField(
|
||||
newText: TextFieldValue,
|
||||
shownIndex: Int,
|
||||
): MultiWalletSeedPhraseUM.GeneratedWordsCheck.WordField {
|
||||
val correct = checkWordField(word = newText.text, shownIndex = shownIndex)
|
||||
|
||||
return copy(
|
||||
word = newText,
|
||||
error = correct.not(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun MultiWalletSeedPhraseUM.GeneratedWordsCheck.allFieldsCorrect(): Boolean {
|
||||
return wordFields.all {
|
||||
checkWordField(
|
||||
word = it.word.text,
|
||||
shownIndex = it.index,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkWordField(word: String, shownIndex: Int): Boolean {
|
||||
val currentState = state.value
|
||||
|
||||
currentState.generatedWords12 ?: return false
|
||||
currentState.generatedWords24 ?: return false
|
||||
|
||||
val wordList = if (currentState.words24Option) {
|
||||
currentState.generatedWords24
|
||||
} else {
|
||||
currentState.generatedWords12
|
||||
}.mnemonicComponents
|
||||
|
||||
return wordList[shownIndex - 1] == word
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.st
|
|||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM.GenerateSeedPhrase.MnemonicGridItem
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun MultiWalletSeedPhraseWords(state: MultiWalletSeedPhraseUM.GenerateSeedPhrase, modifier: Modifier = Modifier) {
|
||||
|
|
@ -37,19 +38,18 @@ fun MultiWalletSeedPhraseWords(state: MultiWalletSeedPhraseUM.GenerateSeedPhrase
|
|||
SeedPhraseGridBlock(
|
||||
mnemonicGridItems = state.words,
|
||||
modifier = Modifier
|
||||
.padding(top = 20.dp, bottom = 92.dp),
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.padding(top = 20.dp, bottom = 32.dp),
|
||||
)
|
||||
|
||||
Box(Modifier.weight(1f)) {
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||
text = stringResource(id = R.string.common_continue),
|
||||
onClick = state.onContinueClick,
|
||||
)
|
||||
}
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||
text = stringResource(id = R.string.common_continue),
|
||||
onClick = state.onContinueClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ private fun SegmentSeedBlock(state: MultiWalletSeedPhraseUM.GenerateSeedPhrase,
|
|||
config = persistentListOf(0, 1),
|
||||
initialSelectedItem = if (state.words24OptionSelected.not()) 0 else 1,
|
||||
onClick = {
|
||||
if (state.words24OptionSelected && it == 1 || state.words24OptionSelected.not() && it == 0) {
|
||||
if (state.words24OptionSelected && it == 0 || state.words24OptionSelected.not() && it == 1) {
|
||||
state.onWords24OptionSwitch()
|
||||
}
|
||||
},
|
||||
|
|
@ -160,10 +160,10 @@ private inline fun <T> VerticalGrid(
|
|||
) {
|
||||
val columnLength = items.size / 2
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
for (index in 0 until 2) {
|
||||
repeat(2) { index ->
|
||||
Column {
|
||||
for (i in 0 until columnLength) {
|
||||
val item = items[index * columnLength + i]
|
||||
|
|
@ -174,34 +174,18 @@ private inline fun <T> VerticalGrid(
|
|||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, heightDp = 640)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
MultiWalletSeedPhraseWords(
|
||||
state = MultiWalletSeedPhraseUM.GenerateSeedPhrase(
|
||||
words = persistentListOf(
|
||||
words = List(24) {
|
||||
MnemonicGridItem(
|
||||
index = 0,
|
||||
index = it + 1,
|
||||
mnemonic = "word1",
|
||||
),
|
||||
MnemonicGridItem(
|
||||
index = 1,
|
||||
mnemonic = "word1",
|
||||
),
|
||||
MnemonicGridItem(
|
||||
index = 2,
|
||||
mnemonic = "word1",
|
||||
),
|
||||
MnemonicGridItem(
|
||||
index = 3,
|
||||
mnemonic = "word1",
|
||||
),
|
||||
MnemonicGridItem(
|
||||
index = 4,
|
||||
mnemonic = "word1",
|
||||
),
|
||||
),
|
||||
)
|
||||
}.toImmutableList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ internal class OnboardingMultiWalletModel @Inject constructor(
|
|||
private fun getInitialStep(): OnboardingMultiWalletState.Step {
|
||||
val card = params.scanResponse.card
|
||||
|
||||
// todo check local storage for scan response
|
||||
return when {
|
||||
card.wallets.isNotEmpty() && card.backupStatus == CardDTO.BackupStatus.NoBackup ->
|
||||
OnboardingMultiWalletState.Step.AddBackupDevice
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue