Updated on 2026-08-14

This commit is contained in:
Tangem 2023-05-16 13:50:36 +04:00
parent c7458d8ab0
commit 35e3ff8ad1
24 changed files with 254 additions and 67 deletions

View file

@ -18,6 +18,12 @@ dependencies {
implementation(project(":core:ui"))
implementation(project(":core:res"))
/** Tangem libraries */
implementation(deps.tangem.card.core)
implementation(deps.tangem.card.android) {
exclude(module = "joda-time")
}
/** AndroidX libraries */
implementation(deps.androidx.core.ktx)
implementation(deps.androidx.appCompat)

View file

@ -1,12 +1,21 @@
package com.tangem.feature.onboarding.api
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.onboarding.presentation.wallet2.ui.*
import com.tangem.feature.onboarding.presentation.wallet2.ui.AboutSeedPhraseScreen
import com.tangem.feature.onboarding.presentation.wallet2.ui.CheckSeedPhraseScreen
import com.tangem.feature.onboarding.presentation.wallet2.ui.ImportSeedPhraseScreen
import com.tangem.feature.onboarding.presentation.wallet2.ui.IntroScreen
import com.tangem.feature.onboarding.presentation.wallet2.ui.YourSeedPhraseScreen
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseScreen
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseViewModel
@ -20,7 +29,7 @@ class OnboardingSeedPhrase : OnboardingSeedPhraseApi {
BackHandler(onBack = viewModel.uiState.onBackClick)
TangemTheme {
Column {
ProgressIndicator(viewModel.progress, maxProgress)
ProgressIndicator(viewModel.progress.collectAsState(0).value, maxProgress)
Content(viewModel)
}
}
@ -48,12 +57,13 @@ private fun ProgressIndicator(progress: Int, maxProgress: Int) {
@Composable
private fun Content(viewModel: SeedPhraseViewModel) {
when (viewModel.currentScreen) {
when (viewModel.currentScreen.collectAsState().value) {
SeedPhraseScreen.Intro -> {
IntroScreen(
state = viewModel.uiState.introState,
)
}
SeedPhraseScreen.AboutSeedPhrase -> {
AboutSeedPhraseScreen(
state = viewModel.uiState.aboutState,

View file

@ -1,21 +1,17 @@
package com.tangem.feature.onboarding.data
import com.tangem.crypto.bip39.*
import com.tangem.datasource.asset.AssetReader
import com.tangem.crypto.bip39.DefaultMnemonic
import com.tangem.crypto.bip39.EntropyLength
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.crypto.bip39.Wordlist
/**
[REDACTED_AUTHOR]
*/
internal class DefaultMnemonicRepository(
private val assetReader: AssetReader,
private val bip39Wordlist: Wordlist,
) : MnemonicRepository {
// TODO: change initialization after adding it to the sdk
private val bip39Wordlist: Wordlist by lazy {
assetReader.openFile(MNEMONIC_FILE_NAME)
.use { BIP39Wordlist(it) }
}
private val bip39Words: Set<String> by lazy { bip39Wordlist.words.toHashSet() }
override fun getWordsDictionary(): Set<String> = bip39Words
@ -25,8 +21,4 @@ internal class DefaultMnemonicRepository(
}
override fun createMnemonic(mnemonicString: String): Mnemonic = DefaultMnemonic(mnemonicString, bip39Wordlist)
companion object {
private const val MNEMONIC_FILE_NAME = "mnemonic/mnemonic_dictionary_en.txt"
}
}

View file

@ -1,8 +1,10 @@
package com.tangem.feature.onboarding.data.di
import com.tangem.datasource.asset.AssetReader
import android.app.Application
import com.tangem.crypto.bip39.Wordlist
import com.tangem.feature.onboarding.data.DefaultMnemonicRepository
import com.tangem.feature.onboarding.data.MnemonicRepository
import com.tangem.sdk.extensions.getWordlist
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -15,7 +17,7 @@ class OnboardingDataModule {
@Provides
@Singleton
fun provideSeedPhraseSdkRepository(assetReader: AssetReader): MnemonicRepository {
return DefaultMnemonicRepository(assetReader)
fun provideSeedPhraseSdkRepository(application: Application): MnemonicRepository {
return DefaultMnemonicRepository(Wordlist.getWordlist(application))
}
}

View file

@ -93,7 +93,6 @@ private fun PhraseBlock(state: ImportSeedPhraseState, modifier: Modifier = Modif
),
colors = TangemTextFieldsDefault.defaultTextFieldColors,
)
Box(
modifier = Modifier

View file

@ -11,4 +11,6 @@ interface SeedPhraseMediator {
fun importWallet(mnemonicComponents: List<String>, callback: (CompletionResult<CreateWalletResponse>) -> Unit)
fun onWalletCreated(result: CompletionResult<CreateWalletResponse>)
fun allowScreenshots(allow: Boolean)
}

View file

@ -4,6 +4,8 @@ import android.net.Uri
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/**
[REDACTED_AUTHOR]
@ -14,15 +16,21 @@ class SeedPhraseRouter(
private val onOpenUriClick: (Uri) -> Unit,
) {
var currentScreen by mutableStateOf(SeedPhraseScreen.Intro)
private val _currentScreen = MutableStateFlow(SeedPhraseScreen.Intro)
val currentScreen: StateFlow<SeedPhraseScreen>
get() = _currentScreen
var currentScreenState by mutableStateOf(SeedPhraseScreen.Intro)
private set
fun navigateBack() {
currentScreen = when (currentScreen) {
_currentScreen.value = when (_currentScreen.value) {
SeedPhraseScreen.Intro -> {
onBack.invoke()
return
}
SeedPhraseScreen.AboutSeedPhrase -> SeedPhraseScreen.Intro
SeedPhraseScreen.YourSeedPhrase -> SeedPhraseScreen.AboutSeedPhrase
SeedPhraseScreen.CheckSeedPhrase -> SeedPhraseScreen.YourSeedPhrase
@ -31,7 +39,7 @@ class SeedPhraseRouter(
}
fun openScreen(screen: SeedPhraseScreen) {
currentScreen = screen
_currentScreen.value = screen
}
fun openChat() {

View file

@ -11,7 +11,17 @@ import com.tangem.common.CompletionResult
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.presentation.wallet2.model.*
import com.tangem.feature.onboarding.presentation.wallet2.model.AboutUiAction
import com.tangem.feature.onboarding.presentation.wallet2.model.CheckSeedPhraseUiAction
import com.tangem.feature.onboarding.presentation.wallet2.model.ImportSeedPhraseUiAction
import com.tangem.feature.onboarding.presentation.wallet2.model.IntroUiAction
import com.tangem.feature.onboarding.presentation.wallet2.model.MnemonicGridItem
import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState
import com.tangem.feature.onboarding.presentation.wallet2.model.SeedPhraseField
import com.tangem.feature.onboarding.presentation.wallet2.model.TextFieldState
import com.tangem.feature.onboarding.presentation.wallet2.model.TextFieldUiAction
import com.tangem.feature.onboarding.presentation.wallet2.model.UiActions
import com.tangem.feature.onboarding.presentation.wallet2.model.YourSeedPhraseUiAction
import com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders.StateBuilder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.Debouncer
@ -19,7 +29,16 @@ import com.tangem.utils.extensions.isEven
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
@ -40,11 +59,11 @@ class SeedPhraseViewModel @Inject constructor(
private lateinit var router: SeedPhraseRouter
private lateinit var mediator: SeedPhraseMediator
val currentScreen: SeedPhraseScreen
val currentScreen: StateFlow<SeedPhraseScreen>
get() = router.currentScreen
val progress: Int
get() = progressByScreen[currentScreen] ?: 0
val progress: Flow<Int>
get() = currentScreen.map { progressByScreen[it] ?: 0 }
val maxProgress: Int by lazy { progressByScreen.maxOfOrNull { it.value } ?: 0 }
@ -150,8 +169,8 @@ class SeedPhraseViewModel @Inject constructor(
val isSameText = textFieldValue.text == oldTextFieldValue.text
val isCursorMoved = textFieldValue.selection != oldTextFieldValue.selection
val mediateState = uiBuilder.importSeedPhrase.updateTextField(uiState, textFieldValue)
uiState = uiBuilder.importSeedPhrase.updateCreateWalletButton(mediateState, enabled = false)
uiState = uiBuilder.importSeedPhrase.updateTextField(uiState, textFieldValue)
// uiState = uiBuilder.importSeedPhrase.updateCreateWalletButton(mediateState, enabled = false)
val fieldState = uiState.importSeedPhraseState.tvSeedPhrase
val inputMnemonic = fieldState.textFieldValue.text
@ -182,16 +201,21 @@ class SeedPhraseViewModel @Inject constructor(
private suspend fun validateMnemonic(inputMnemonic: String) {
if (inputMnemonic.isEmpty() && uiState.importSeedPhraseState.error != null) {
updateUi { uiBuilder.importSeedPhrase.updateError(uiState, null) }
updateUi {
uiState = uiBuilder.importSeedPhrase.updateCreateWalletButton(uiState, enabled = false)
uiBuilder.importSeedPhrase.updateError(uiState, null)
}
return
}
interactor.validateMnemonicString(inputMnemonic)
.onSuccess { mnemonicComponents ->
importedMnemonicComponents = mnemonicComponents
uiState = uiBuilder.importSeedPhrase.updateCreateWalletButton(uiState, enabled = true)
updateUi { uiBuilder.importSeedPhrase.updateError(uiState, null) }
}
.onFailure {
uiState = uiBuilder.importSeedPhrase.updateCreateWalletButton(uiState, enabled = false)
val error = it as? SeedPhraseError ?: return
importedMnemonicComponents = null
@ -199,6 +223,7 @@ class SeedPhraseViewModel @Inject constructor(
is SeedPhraseError.InvalidWords -> {
uiBuilder.importSeedPhrase.updateInvalidWords(uiState, error.words)
}
else -> uiState
}
updateUi { uiBuilder.importSeedPhrase.updateError(mediateState, error) }
@ -266,6 +291,7 @@ class SeedPhraseViewModel @Inject constructor(
.onSuccess { mnemonic ->
generatedMnemonicComponents = mnemonic.mnemonicComponents
val mnemonicGridItems = generateMnemonicGridList(mnemonic.mnemonicComponents)
setScreenshotsEnabled()
updateUi {
router.openScreen(SeedPhraseScreen.YourSeedPhrase)
uiBuilder.mnemonicGenerated(uiState, mnemonicGridItems.toImmutableList())
@ -278,6 +304,13 @@ class SeedPhraseViewModel @Inject constructor(
}
}
private fun setScreenshotsEnabled() {
router.currentScreen.onEach {
val allowScreenshots = it != SeedPhraseScreen.YourSeedPhrase
mediator.allowScreenshots(allowScreenshots)
}.launchIn(viewModelScope)
}
private suspend fun generateMnemonicGridList(mnemonicComponents: List<String>): ImmutableList<MnemonicGridItem> {
val size = mnemonicComponents.size
val splitIndex = if (size.isEven()) size / 2 else size / 2 + 1
@ -310,6 +343,7 @@ class SeedPhraseViewModel @Inject constructor(
private fun buttonContinueClick() {
router.openScreen(SeedPhraseScreen.CheckSeedPhrase)
mediator.allowScreenshots(true)
}
private fun buttonSuggestedPhraseClick(suggestionIndex: Int) {
@ -368,7 +402,8 @@ class SeedPhraseViewModel @Inject constructor(
// endregion Utils
companion object {
private val URI_ABOUT_SEED_PHRASE = Uri.parse("https://ya.ru")
private val URI_ABOUT_SEED_PHRASE =
Uri.parse("https://tangem.com/ru/blog/post/seed-phrase-a-risky-solution/")
private const val MNEMONIC_DEBOUNCER = "MnemonicDebouncer"
private const val MNEMONIC_DEBOUNCE_DELAY = 700L
private const val DELAY_GENERATE_SEED_PHRASE = 300L