Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-25 13:04:54 +03:00
parent cf05b371ef
commit e080401183
103 changed files with 142 additions and 10555 deletions

View file

@ -1,8 +1,5 @@
package com.tangem.features.onboarding.v2
interface OnboardingV2FeatureToggles {
val isOnboardingV2Enabled: Boolean
val isVisaOnboardingEnabled: Boolean
val isNoteRefactoringEnabled: Boolean
val isTwinRefactoringEnabled: Boolean
}

View file

@ -6,12 +6,6 @@ import javax.inject.Inject
internal class DefaultOnboardingV2FeatureToggles @Inject constructor(
private val featureTogglesManager: FeatureTogglesManager,
) : OnboardingV2FeatureToggles {
override val isOnboardingV2Enabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("ONBOARDING_CODE_REFACTORING_ENABLED")
override val isVisaOnboardingEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("VISA_ONBOARDING_ENABLED")
override val isNoteRefactoringEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("NOTE_REFACTORING_ENABLED")
override val isTwinRefactoringEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("TWIN_REFACTORING_ENABLED")
}

View file

@ -1 +0,0 @@
/build

View file

@ -1,64 +0,0 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.kotlin.serialization)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.feature.onboarding"
}
dependencies {
/** Core modules */
implementation(project(":common"))
implementation(project(":domain:models"))
implementation(project(":core:config-toggles"))
implementation(project(":core:datasource"))
implementation(project(":core:analytics"))
implementation(projects.core.analytics.models)
implementation(project(":core:utils"))
implementation(project(":core:ui"))
implementation(project(":core:res"))
implementation(project(":data:common"))
/** Tangem libraries */
implementation(tangemDeps.card.core)
implementation(tangemDeps.card.android) {
exclude(module = "joda-time")
}
/** AndroidX libraries */
implementation(deps.androidx.core.ktx)
implementation(deps.androidx.appCompat)
implementation(deps.androidx.fragment.ktx)
implementation(deps.androidx.constraintLayout)
implementation(deps.androidx.activity.compose)
implementation(deps.lifecycle.runtime.ktx)
implementation(deps.lifecycle.common.java8)
implementation(deps.lifecycle.viewModel.ktx)
/** Compose libraries */
implementation(deps.compose.material)
implementation(deps.compose.material3)
implementation(deps.compose.animation)
implementation(deps.compose.foundation)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.coil)
implementation(deps.compose.shimmer)
implementation(tangemDeps.card.core)
/** Other libraries */
implementation(deps.compose.shimmer)
implementation(deps.kotlin.immutable.collections)
implementation(deps.kotlin.serialization)
implementation(deps.timber)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
}

View file

@ -1,14 +0,0 @@
package com.tangem.feature.onboarding.api
import androidx.compose.runtime.Composable
import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseScreen
/**
[REDACTED_AUTHOR]
*/
interface OnboardingSeedPhraseApi {
@Suppress("TopLevelComposableFunctions")
@Composable
fun ScreenContent(uiState: OnboardingSeedPhraseState, subScreen: SeedPhraseScreen, progress: Float)
}

View file

@ -1,71 +0,0 @@
package com.tangem.feature.onboarding.api
import androidx.activity.compose.BackHandler
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.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState
import com.tangem.feature.onboarding.presentation.wallet2.ui.*
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseScreen
/**
[REDACTED_AUTHOR]
*/
class OnboardingSeedPhraseScreen : OnboardingSeedPhraseApi {
@Composable
override fun ScreenContent(uiState: OnboardingSeedPhraseState, subScreen: SeedPhraseScreen, progress: Float) {
BackHandler(onBack = uiState.onBackClick)
Column {
TangemLinearProgressIndicator(
progress = { progress },
color = TangemTheme.colors.icon.primary1,
backgroundColor = TangemTheme.colors.icon.primary1.copy(alpha = 0.4f),
strokeCap = StrokeCap.Round,
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(top = TangemTheme.dimens.size8)
.padding(horizontal = TangemTheme.dimens.size16),
)
Content(subScreen, uiState)
}
}
}
@Composable
private fun Content(screen: SeedPhraseScreen, uiState: OnboardingSeedPhraseState) {
when (screen) {
SeedPhraseScreen.Intro -> {
IntroScreen(
state = uiState.introState,
)
}
SeedPhraseScreen.AboutSeedPhrase -> {
AboutSeedPhraseScreen(
state = uiState.aboutState,
)
}
SeedPhraseScreen.YourSeedPhrase -> {
YourSeedPhraseScreen(
state = uiState.yourSeedPhraseState,
)
}
SeedPhraseScreen.CheckSeedPhrase -> {
CheckSeedPhraseScreen(
state = uiState.checkSeedPhraseState,
)
}
SeedPhraseScreen.ImportSeedPhrase -> {
ImportSeedPhraseScreen(
state = uiState.importSeedPhraseState,
)
}
}
}

View file

@ -1,32 +0,0 @@
package com.tangem.feature.onboarding.data
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]
*/
class DefaultMnemonicRepository(
private val bip39Wordlist: Wordlist,
) : MnemonicRepository {
private val bip39Words: Set<String> by lazy { bip39Wordlist.words.toHashSet() }
override fun getWordsDictionary(): Set<String> = bip39Words
override fun generateDefaultMnemonic(): Mnemonic {
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

@ -1,22 +0,0 @@
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]
*/
interface MnemonicRepository {
fun getWordsDictionary(): Set<String>
@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

@ -1,15 +0,0 @@
package com.tangem.feature.onboarding.data.model
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.domain.models.scan.CardDTO
import com.tangem.operations.backup.PrimaryCard
import com.tangem.operations.derivation.ExtendedPublicKeysMap
/**
[REDACTED_AUTHOR]
*/
data class CreateWalletResponse(
val card: CardDTO,
val derivedKeys: Map<ByteArrayKey, ExtendedPublicKeysMap> = mapOf(),
val primaryCard: PrimaryCard? = null,
)

View file

@ -1,20 +0,0 @@
package com.tangem.feature.onboarding.di
import com.tangem.feature.onboarding.navigation.DefaultOnboardingRouter
import com.tangem.feature.onboarding.navigation.OnboardingRouter
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.scopes.ActivityScoped
@Module
@InstallIn(ActivityComponent::class)
internal object OnboardingRouterModule {
@Provides
@ActivityScoped
fun provideOnboardingRouter(): OnboardingRouter {
return DefaultOnboardingRouter()
}
}

View file

@ -1,113 +0,0 @@
package com.tangem.feature.onboarding.domain
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
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
/**
[REDACTED_AUTHOR]
*/
internal class DefaultSeedPhraseInteractor(
private val repository: MnemonicRepository,
) : SeedPhraseInteractor {
private val partWordFinder: PartWordFinder = PartWordFinder()
override suspend fun generateMnemonic(): Result<Mnemonic> {
return runCatching {
repository.generateDefaultMnemonic()
}
}
override suspend fun generateMnemonics(mnemonicTypes: List<MnemonicType>): Result<Map<MnemonicType, Mnemonic>> {
return runCatching {
mnemonicTypes.associateWith {
repository.generateMnemonic(it)
}
}
}
override suspend fun isWordMatch(mnemonicComponents: List<String>?, field: SeedPhraseField, word: String): Boolean {
return mnemonicComponents?.get(field.index) == word
}
override suspend fun validateMnemonicString(text: String): Result<List<String>> {
val inputWords = text.split("\\s+".toRegex()).toSet()
val wordsDictionary = repository.getWordsDictionary()
val invalidWords = inputWords
.filter { it.isNotEmpty() && !wordsDictionary.contains(it) }
.toSet()
return if (invalidWords.isNotEmpty()) {
Result.failure(SeedPhraseError.InvalidWords(invalidWords))
} else {
try {
val mnemonic = repository.createMnemonic(text)
Result.success(mnemonic.mnemonicComponents)
} catch (ex: TangemSdkError.MnemonicException) {
val error = ex.mnemonicResult.mapToError()
Result.failure(error)
}
}
}
override suspend fun getSuggestions(
text: String,
hasSelection: Boolean,
cursorPosition: Int,
): ImmutableList<String> {
if (text.isEmpty() || cursorPosition == 0 || hasSelection) return persistentListOf()
val word = partWordFinder.getLeadPartOfWord(text, cursorPosition)
?: return persistentListOf()
val suggestions = repository.getWordsDictionary()
.filter { it.startsWith(word, ignoreCase = false) && it != word }
.toPersistentList()
return suggestions
}
override suspend fun insertSuggestionWord(
text: String,
suggestion: String,
cursorPosition: Int,
): InsertSuggestionResult {
val textPartLead = text.substring(0, cursorPosition)
val textPartLast = text.substring(cursorPosition, text.length)
val wordPartLast = partWordFinder.getLastPartOfWord(text, suggestion, cursorPosition)
val textBuilder = StringBuilder()
.append(textPartLead)
.append(wordPartLast)
if (textPartLast.isEmpty() || textPartLast[0].isNotWhitespace()) {
textBuilder.append(SeedPhraseInteractor.MNEMONIC_DELIMITER)
}
textBuilder.append(textPartLast)
val textWithSuggestion = textBuilder.toString()
val newCursorPosition = cursorPosition + wordPartLast.length + SeedPhraseInteractor.MNEMONIC_DELIMITER.length
return InsertSuggestionResult(textWithSuggestion, newCursorPosition)
}
}
private fun MnemonicErrorResult.mapToError(): SeedPhraseError = when (this) {
MnemonicErrorResult.InvalidWordCount -> SeedPhraseError.InvalidWordCount
MnemonicErrorResult.InvalidEntropyLength -> SeedPhraseError.InvalidEntropyLength
MnemonicErrorResult.InvalidWordsFile -> SeedPhraseError.InvalidWordsFile
MnemonicErrorResult.InvalidChecksum -> SeedPhraseError.InvalidChecksum
MnemonicErrorResult.MnenmonicCreationFailed -> SeedPhraseError.MnenmonicCreationFailed
MnemonicErrorResult.NormalizationFailed -> SeedPhraseError.NormalizationFailed
MnemonicErrorResult.UnsupportedLanguage -> SeedPhraseError.UnsupportedLanguage
is MnemonicErrorResult.InvalidWords -> SeedPhraseError.InvalidWords(this.words)
MnemonicErrorResult.InvalidMnemonic -> SeedPhraseError.InvalidMnemonic
}

View file

@ -1,41 +0,0 @@
package com.tangem.feature.onboarding.domain
import com.tangem.common.module.ModuleError
import com.tangem.common.module.ModuleErrorCode
import com.tangem.common.module.ModuleMessage
/**
[REDACTED_AUTHOR]
* For each pool of errors, use the codes lying in range 100 (200..299, 300..399, etc)
*/
abstract class OnboardingModuleError(
subCode: Int,
override val message: String,
override val data: Any?,
) : ModuleMessage, ModuleError() {
override val code: Int = ModuleErrorCode.ONBOARDING + subCode
companion object {
internal const val ERROR_CODE_SEED_PHRASE = 100
}
}
sealed class SeedPhraseError(
subCode: Int = 0,
message: String? = null,
data: Any? = null,
) : OnboardingModuleError(
subCode = ERROR_CODE_SEED_PHRASE + subCode,
message = message ?: this::class.java.simpleName,
data = data,
) {
object InvalidWordCount : SeedPhraseError(subCode = 0)
object InvalidEntropyLength : SeedPhraseError(subCode = 1)
object InvalidWordsFile : SeedPhraseError(subCode = 2)
object InvalidChecksum : SeedPhraseError(subCode = 3)
object MnenmonicCreationFailed : SeedPhraseError(subCode = 4)
object NormalizationFailed : SeedPhraseError(subCode = 5)
object UnsupportedLanguage : SeedPhraseError(subCode = 6)
data class InvalidWords(val words: Set<String>) : SeedPhraseError(subCode = 7)
object InvalidMnemonic : SeedPhraseError(subCode = 8)
}

View file

@ -1,50 +0,0 @@
package com.tangem.feature.onboarding.domain
import com.tangem.utils.extensions.isNotWhitespace
/**
[REDACTED_AUTHOR]
* Class which provides helper functions to determine lead or last part of words for suggestions
*/
internal class PartWordFinder {
/**
* Searches and return part of a word on the left side of the [cursorPosition] into the [text]
*/
fun getLeadPartOfWord(text: String, cursorPosition: Int): String? {
val charText = text.toCharArray().asList()
val cursorIsOutOfTextRange = cursorPosition > charText.size || cursorPosition < 0
val cursorAtStartOfText = cursorPosition == 0
val hadWhiteSpaceOnLeftOfCursor = charText[cursorPosition - 1].isWhitespace()
val cursorInMiddleOfWord = charText.size > cursorPosition && charText[cursorPosition].isNotWhitespace()
@Suppress("ComplexCondition")
if (cursorIsOutOfTextRange || cursorAtStartOfText || hadWhiteSpaceOnLeftOfCursor || cursorInMiddleOfWord) {
return null
}
for (charIndex in cursorPosition - 1 downTo 0) {
val char = charText[charIndex]
when {
charIndex == 0 -> {
return text.substring(charIndex, cursorPosition)
}
char.isWhitespace() -> {
return text.substring(charIndex + 1, cursorPosition)
}
}
}
return null
}
/**
* Returns a substring of the [word] which excludes the first word part found on the left of [cursorPosition] in
* the [text]
*/
fun getLastPartOfWord(text: String, word: String, cursorPosition: Int): String {
val leadPart = getLeadPartOfWord(text, cursorPosition) ?: return ""
return word.substring(leadPart.length, word.length)
}
}

View file

@ -1,27 +0,0 @@
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
/**
[REDACTED_AUTHOR]
*/
interface SeedPhraseInteractor {
suspend fun generateMnemonic(): Result<Mnemonic>
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>
suspend fun insertSuggestionWord(text: String, suggestion: String, cursorPosition: Int): InsertSuggestionResult
companion object {
const val MNEMONIC_DELIMITER = " "
}
}
data class InsertSuggestionResult(
val text: String,
val cursorPosition: Int,
)

View file

@ -1,21 +0,0 @@
package com.tangem.feature.onboarding.domain.di
import com.tangem.feature.onboarding.data.MnemonicRepository
import com.tangem.feature.onboarding.domain.DefaultSeedPhraseInteractor
import com.tangem.feature.onboarding.domain.SeedPhraseInteractor
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class OnboardingDomainModule {
@Provides
@Singleton
fun provideSeedPhraseInteractor(repository: MnemonicRepository): SeedPhraseInteractor {
return DefaultSeedPhraseInteractor(repository)
}
}

View file

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

View file

@ -1,3 +0,0 @@
package com.tangem.feature.onboarding.navigation
class DefaultOnboardingRouter : OnboardingRouter

View file

@ -1,7 +0,0 @@
package com.tangem.feature.onboarding.navigation
/**
* Onboarding router
*/
// TODO: Move to onboarding api module [REDACTED_JIRA]
interface OnboardingRouter

View file

@ -1,41 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
sealed class SeedPhraseEvents(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(ONBOARDING_SEED_PHRASE, event, params) {
object ButtonGenerateSeedPhrase : SeedPhraseEvents("Button - Generate Seed Phrase")
object ButtonImportWallet : SeedPhraseEvents("Button - Import Wallet")
object ButtonReadMore : SeedPhraseEvents("Button - Read More")
object ButtonImport : SeedPhraseEvents("Button - Import")
object IntroScreenOpened : SeedPhraseEvents("Seed Intro Screen Opened")
object GenerationScreenOpened : SeedPhraseEvents("Seed Generation Screen Opened")
object CheckingScreenOpened : SeedPhraseEvents("Seed Checking Screen Opened")
object ImportScreenOpened : SeedPhraseEvents("Import Seed Phrase Screen Opened")
companion object {
const val ONBOARDING_SEED_PHRASE = "Onboarding / Seed Phrase"
}
}
sealed class CreateWalletEvents(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(ONBOARDING_CREATE_WALLET, event, params) {
object OnboardingSeedButtonCreateWallet : CreateWalletEvents("Button - Create Wallet")
object OnboardingSeedButtonOtherOptions : CreateWalletEvents("Button - Other Options")
companion object {
const val ONBOARDING_CREATE_WALLET = "Onboarding / Create Wallet"
}
}
enum class SeedPhraseSource {
IMPORTED, GENERATED
}

View file

@ -1,11 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.model
/**
[REDACTED_AUTHOR]
*/
data class ButtonState(
val enabled: Boolean = true,
val isClickable: Boolean = true,
val showProgress: Boolean = false,
val onClick: () -> Unit,
)

View file

@ -1,8 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.model
import androidx.annotation.StringRes
data class DescriptionResource(
@StringRes val titleRes: Int,
@StringRes val subTitleRes: Int,
)

View file

@ -1,102 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.model
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.feature.onboarding.domain.SeedPhraseError
import com.tangem.feature.onboarding.domain.models.MnemonicType
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
/**
[REDACTED_AUTHOR]
*/
data class OnboardingSeedPhraseState(
val introState: IntroState,
val aboutState: AboutState,
val yourSeedPhraseState: YourSeedPhraseState,
val checkSeedPhraseState: CheckSeedPhraseState,
val importSeedPhraseState: ImportSeedPhraseState,
val menuButtonChat: ButtonState,
val onBackClick: () -> Unit,
)
data class IntroState(
val cardImageUrl: String? = null,
val buttonCreateWallet: ButtonState,
val buttonOtherOptions: ButtonState,
)
data class AboutState(
val buttonReadMoreAboutSeedPhrase: ButtonState,
val buttonGenerateSeedPhrase: ButtonState,
val buttonImportSeedPhrase: ButtonState,
)
data class YourSeedPhraseState(
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 MnemonicGridItem(
val index: Int,
val mnemonic: String,
)
data class CheckSeedPhraseState(
val tvSecondPhrase: TextFieldState,
val tvSeventhPhrase: TextFieldState,
val tvEleventhPhrase: TextFieldState,
val buttonCreateWallet: ButtonState,
)
data class ImportSeedPhraseState(
val fieldSeedPhrase: TextFieldState,
val fieldPassphrase: TextFieldState,
val onSuggestedPhraseClick: (Int) -> Unit,
val onPassphraseInfoClick: () -> Unit,
val buttonCreateWallet: ButtonState,
val invalidWords: Set<String> = emptySet(),
val suggestionsList: ImmutableList<String> = persistentListOf(),
val error: SeedPhraseError? = null,
val bottomSheetConfig: TangemBottomSheetConfig? = null,
)
data class ShowPassphraseInfoBottomSheetContent(val onOkClick: () -> Unit) : TangemBottomSheetConfigContent
data class TextFieldState(
val onTextFieldValueChanged: (TextFieldValue) -> Unit,
val textFieldValue: TextFieldValue = TextFieldValue(),
val label: String? = null,
val isError: Boolean = false,
val isFocused: Boolean = false,
val onFocusChanged: (Boolean) -> Unit = {},
)

View file

@ -1,60 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.model
import androidx.compose.ui.text.input.TextFieldValue
/**
[REDACTED_AUTHOR]
*/
data class UiActions(
val introActions: IntroUiAction,
val aboutActions: AboutUiAction,
val yourSeedPhraseActions: YourSeedPhraseUiAction,
val checkSeedPhraseActions: CheckSeedPhraseUiAction,
val importSeedPhraseActions: ImportSeedPhraseUiAction,
val menuChatClick: () -> Unit,
val menuNavigateBackClick: () -> Unit,
)
data class IntroUiAction(
val buttonCreateWalletClick: () -> Unit,
val buttonOtherOptionsClick: () -> Unit,
)
data class AboutUiAction(
val buttonReadMoreAboutSeedPhraseClick: () -> Unit,
val buttonGenerateSeedPhraseClick: () -> Unit,
val buttonImportSeedPhraseClick: () -> Unit,
)
data class YourSeedPhraseUiAction(
val buttonContinueClick: () -> Unit,
val onSelectType: (SegmentSeedType) -> Unit,
)
data class CheckSeedPhraseUiAction(
val secondTextFieldAction: TextFieldUiAction,
val seventhTextFieldAction: TextFieldUiAction,
val eleventhTextFieldAction: TextFieldUiAction,
val buttonCreateWalletClick: () -> Unit,
)
data class ImportSeedPhraseUiAction(
val phraseTextFieldAction: TextFieldUiAction,
val passTextFieldAction: TextFieldUiAction,
val suggestedPhraseClick: (Int) -> Unit,
val onPassphraseInfoClick: () -> Unit,
val buttonCreateWalletClick: () -> Unit,
)
data class TextFieldUiAction(
val onTextFieldChanged: (TextFieldValue) -> Unit,
val onFocusChanged: (Boolean) -> Unit = {},
)
enum class SeedPhraseField(val index: Int) {
Second(index = 1),
Seventh(index = 6),
Eleventh(index = 10),
}

View file

@ -1,151 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.components.SpacerW8
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.onboarding.R
import com.tangem.feature.onboarding.presentation.wallet2.model.AboutState
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
/**
[REDACTED_AUTHOR]
*/
@Composable
fun AboutSeedPhraseScreen(state: AboutState, modifier: Modifier = Modifier) {
Column(
modifier = modifier.fillMaxSize(),
) {
Box(
modifier = Modifier.weight(weight = 0.8f),
) {
EditIconBlock()
}
Box(
modifier = Modifier.weight(weight = 1.2f),
) {
Column {
OnboardingDescriptionBlock {
Description(
titleRes = R.string.onboarding_seed_intro_title,
subTitleRes = R.string.onboarding_seed_intro_message,
)
}
ReadMoreBlock(state)
}
}
Box {
OnboardingActionBlock(
firstActionContent = {
SecondaryButton(
modifier = Modifier
.fillMaxWidth(),
text = stringResourceSafe(id = R.string.onboarding_seed_intro_button_generate),
enabled = state.buttonGenerateSeedPhrase.enabled,
showProgress = state.buttonGenerateSeedPhrase.showProgress,
onClick = state.buttonGenerateSeedPhrase.onClick,
)
},
secondActionContent = {
SecondaryButton(
modifier = Modifier
.fillMaxWidth(),
text = stringResourceSafe(id = R.string.onboarding_seed_intro_button_import),
enabled = state.buttonImportSeedPhrase.enabled,
showProgress = state.buttonImportSeedPhrase.showProgress,
onClick = state.buttonImportSeedPhrase.onClick,
)
},
)
}
}
}
@Composable
private fun EditIconBlock() {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Image(
modifier = Modifier
.size(TangemTheme.dimens.size44),
painter = painterResource(id = R.drawable.ic_onboarding_text_edit_56),
contentDescription = null,
)
SpacerH32()
Box(
modifier = Modifier
.background(
color = TangemTheme.colors.icon.warning.copy(alpha = 0.12f),
shape = RoundedCornerShape(TangemTheme.dimens.radius8),
),
) {
Text(
modifier = Modifier
.padding(
horizontal = TangemTheme.dimens.size12,
vertical = TangemTheme.dimens.size4,
),
text = stringResourceSafe(id = R.string.onboarding_seed_phrase_intro_legacy),
color = TangemTheme.colors.text.warning,
style = TangemTheme.typography.body1,
)
}
}
}
@Composable
private fun ReadMoreBlock(state: AboutState) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens.size16),
) {
val shape = TangemTheme.shapes.roundedCornersLarge
Row(
modifier = Modifier
.align(Alignment.Center)
.clip(shape)
.border(
width = TangemTheme.dimens.size1,
color = TangemTheme.colors.stroke.primary,
shape = shape,
)
.clickable(onClick = state.buttonReadMoreAboutSeedPhrase.onClick)
.padding(
horizontal = TangemTheme.dimens.size16,
vertical = TangemTheme.dimens.size8,
),
) {
Icon(
painter = painterResource(id = R.drawable.ic_arrow_top_right_24),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
)
SpacerW8()
Text(
text = stringResourceSafe(id = R.string.onboarding_seed_button_read_more),
color = TangemTheme.colors.text.primary1,
)
}
}
}

View file

@ -1,110 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.core.ui.R
import com.tangem.core.ui.components.OutlineTextField
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.fields.contextmenu.DisableContextMenu
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.onboarding.presentation.wallet2.model.CheckSeedPhraseState
import com.tangem.feature.onboarding.presentation.wallet2.model.TextFieldState
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
/**
[REDACTED_AUTHOR]
*/
@Suppress("ReusedModifierInstance")
@Composable
fun CheckSeedPhraseScreen(state: CheckSeedPhraseState, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize(),
) {
LazyColumn(
modifier = modifier
.weight(1f)
.fillMaxWidth(),
) {
item {
OnboardingDescriptionBlock(
modifier = Modifier
.weight(1f)
.padding(top = TangemTheme.dimens.size48)
.padding(horizontal = TangemTheme.dimens.size16),
) {
Description(
titleRes = R.string.onboarding_seed_user_validation_title,
subTitleRes = R.string.onboarding_seed_user_validation_message,
)
}
}
item {
CheckSeedPhraseBlock(
modifier = Modifier
.imePadding()
.weight(1f)
.padding(top = TangemTheme.dimens.size20)
.padding(horizontal = TangemTheme.dimens.size16),
state = state,
)
}
}
OnboardingActionBlock(
firstActionContent = {
PrimaryButtonIconEnd(
modifier = Modifier
.fillMaxWidth(),
text = stringResourceSafe(id = R.string.onboarding_create_wallet_button_create_wallet),
iconResId = R.drawable.ic_tangem_24,
enabled = state.buttonCreateWallet.enabled,
showProgress = state.buttonCreateWallet.showProgress,
onClick = state.buttonCreateWallet.onClick,
)
},
)
}
}
@Composable
private fun CheckSeedPhraseBlock(state: CheckSeedPhraseState, modifier: Modifier = Modifier) {
Column(modifier) {
CheckSeedPhraseTextField(state = state.tvSecondPhrase, imeAction = ImeAction.Next)
CheckSeedPhraseTextField(state = state.tvSeventhPhrase, imeAction = ImeAction.Next)
CheckSeedPhraseTextField(state = state.tvEleventhPhrase, imeAction = ImeAction.Done)
}
}
@Composable
private fun CheckSeedPhraseTextField(state: TextFieldState, imeAction: ImeAction) {
val focusManager = LocalFocusManager.current
DisableContextMenu {
OutlineTextField(
value = state.textFieldValue,
onValueChange = state.onTextFieldValueChanged,
label = state.label,
isError = state.isError,
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password,
imeAction = imeAction,
),
keyboardActions = KeyboardActions(
onDone = { focusManager.clearFocus() },
onNext = { focusManager.moveFocus(focusDirection = FocusDirection.Down) },
),
)
}
}

View file

@ -1,268 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui
import android.content.res.Configuration
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import com.tangem.core.ui.components.*
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.onboarding.R
import com.tangem.feature.onboarding.presentation.wallet2.model.ButtonState
import com.tangem.feature.onboarding.presentation.wallet2.model.ImportSeedPhraseState
import com.tangem.feature.onboarding.presentation.wallet2.model.TextFieldState
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.DescriptionSubTitleText
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.InvalidWordsColorTransformation
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseErrorConverter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/**
[REDACTED_AUTHOR]
*/
@Composable
fun ImportSeedPhraseScreen(state: ImportSeedPhraseState, modifier: Modifier = Modifier) {
val keyboard by keyboardAsState()
Box(
modifier = modifier.fillMaxSize(),
) {
Column {
OnboardingDescriptionBlock(
modifier = Modifier.padding(top = TangemTheme.dimens.size16),
) {
DescriptionSubTitleText(text = stringResourceSafe(id = R.string.onboarding_seed_import_message))
}
PhraseBlock(
modifier = Modifier
.padding(top = TangemTheme.dimens.size62)
.padding(horizontal = TangemTheme.dimens.size16),
state = state,
)
PassphraseBlock(
modifier = Modifier.padding(horizontal = TangemTheme.dimens.size16),
passphraseField = state.fieldPassphrase,
onPassphraseInfoClick = state.onPassphraseInfoClick,
)
OnboardingActionBlock(
modifier = Modifier.padding(top = TangemTheme.dimens.size16),
firstActionContent = {
PrimaryButtonIconEnd(
modifier = Modifier
.fillMaxWidth(),
text = stringResourceSafe(id = R.string.common_import),
iconResId = R.drawable.ic_tangem_24,
enabled = state.buttonCreateWallet.enabled,
showProgress = state.buttonCreateWallet.showProgress,
onClick = state.buttonCreateWallet.onClick,
)
},
)
}
if (keyboard is Keyboard.Opened) {
SuggestionsBlock(
modifier = Modifier
.align(Alignment.BottomCenter)
.imePadding(),
suggestionsList = state.suggestionsList,
onClick = { index -> state.onSuggestedPhraseClick(index) },
)
}
if (state.bottomSheetConfig != null) {
PassphraseInfoBottomSheet(config = state.bottomSheetConfig)
}
}
}
@Composable
private fun PassphraseBlock(
passphraseField: TextFieldState,
onPassphraseInfoClick: () -> Unit,
modifier: Modifier = Modifier,
) {
OutlineTextFieldWithIcon(
modifier = modifier.fillMaxWidth(),
value = passphraseField.textFieldValue,
onValueChange = passphraseField.onTextFieldValueChanged,
iconResId = R.drawable.ic_information_24,
iconColor = TangemTheme.colors.icon.informative,
label = stringResourceSafe(id = R.string.common_passphrase),
placeholder = stringResourceSafe(id = R.string.send_optional_field),
onIconClick = onPassphraseInfoClick,
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password,
),
)
}
@Composable
private fun PhraseBlock(state: ImportSeedPhraseState, modifier: Modifier = Modifier) {
// TODO: replace by TextReference
val errorConverter = remember { SeedPhraseErrorConverter() }
Column(
modifier = modifier.fillMaxWidth(),
) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size142),
value = state.fieldSeedPhrase.textFieldValue,
onValueChange = state.fieldSeedPhrase.onTextFieldValueChanged,
textStyle = TangemTheme.typography.body1,
singleLine = false,
visualTransformation = InvalidWordsColorTransformation(
wordsToBrush = state.invalidWords,
style = SpanStyle(color = TangemTheme.colors.text.warning),
),
colors = TangemTextFieldsDefault.defaultTextFieldColors,
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password,
),
)
Box(
modifier = Modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size32),
) {
val message = errorConverter.convert(LocalContext.current to state.error)
if (message != null) {
Text(
modifier = Modifier.fillMaxSize(),
text = message,
style = TangemTheme.typography.caption2.copy(
color = TangemTheme.colors.text.warning,
),
)
}
}
}
}
@Suppress("ReusedModifierInstance")
@Composable
private fun SuggestionsBlock(
suggestionsList: ImmutableList<String>,
onClick: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
modifier = modifier,
enter = fadeIn() + slideIn(initialOffset = { IntOffset(x = 200, y = 0) }),
exit = slideOut(targetOffset = { IntOffset(x = -200, y = 0) }) + fadeOut(),
visible = suggestionsList.isNotEmpty(),
) {
LazyRow(
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.size16),
) {
items(suggestionsList.size) { index ->
SuggestionButton(
modifier = Modifier.rowPadding(
index = index,
rowSize = suggestionsList.size,
outSide = TangemTheme.dimens.size0,
inSide = TangemTheme.dimens.size4,
),
text = suggestionsList[index],
onClick = { onClick(index) },
)
}
}
}
}
@Composable
private fun SuggestionButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
Box(modifier = modifier.clickable(onClick = onClick)) {
Notifier(
text = text,
backgroundColor = TangemTheme.colors.icon.primary1,
textColor = TangemTheme.colors.text.primary2,
)
}
}
private fun Modifier.rowPadding(index: Int, rowSize: Int, outSide: Dp, inSide: Dp): Modifier = when (index) {
0 -> this.padding(start = outSide, end = inSide)
rowSize - 1 -> this.padding(start = inSide, end = outSide)
else -> this.padding(horizontal = inSide)
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun SuggestionsBlockPreview(
@PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList<String>,
) {
TangemThemePreview {
SuggestionsBlock(
suggestionsList = suggestions,
onClick = {},
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ImportSeedPhraseScreenPreview(
@PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList<String>,
) {
TangemThemePreview {
Box(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
ImportSeedPhraseScreen(
ImportSeedPhraseState(
fieldSeedPhrase = TextFieldState(onTextFieldValueChanged = {}),
fieldPassphrase = TextFieldState(onTextFieldValueChanged = {}),
onSuggestedPhraseClick = {},
onPassphraseInfoClick = {},
buttonCreateWallet = ButtonState(
enabled = true,
isClickable = true,
showProgress = false,
onClick = {},
),
suggestionsList = suggestions,
),
)
}
}
}
private class SuggestionsPreviewParamsProvider : CollectionPreviewParameterProvider<ImmutableList<String>>(
collection = listOf(
persistentListOf(
"one",
"each",
"explorer",
"unknown",
),
),
)

View file

@ -1,102 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.onboarding.R
import com.tangem.feature.onboarding.presentation.wallet2.model.IntroState
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
/**
[REDACTED_AUTHOR]
*/
@Composable
fun IntroScreen(state: IntroState, modifier: Modifier = Modifier) {
Column(
modifier = modifier.fillMaxSize(),
) {
Box(
modifier = Modifier.weight(weight = 1.5f),
) {
CardImageBlock(state.cardImageUrl)
}
Box(
modifier = Modifier.weight(weight = 1f),
) {
OnboardingDescriptionBlock {
Description(
titleRes = R.string.onboarding_create_wallet_options_title,
subTitleRes = R.string.onboarding_create_wallet_options_message,
)
}
}
Box {
OnboardingActionBlock(
firstActionContent = {
PrimaryButtonIconEnd(
modifier = Modifier
.fillMaxWidth(),
text = stringResourceSafe(id = R.string.onboarding_create_wallet_button_create_wallet),
iconResId = R.drawable.ic_tangem_24,
enabled = state.buttonCreateWallet.enabled,
showProgress = state.buttonCreateWallet.showProgress,
onClick = state.buttonCreateWallet.onClick,
)
},
secondActionContent = {
SecondaryButton(
modifier = Modifier
.fillMaxWidth(),
text = stringResourceSafe(id = R.string.onboarding_create_wallet_options_button_options),
enabled = state.buttonOtherOptions.enabled,
showProgress = state.buttonOtherOptions.showProgress,
onClick = state.buttonOtherOptions.onClick,
)
},
)
}
}
}
@Composable
private fun CardImageBlock(cardImageUrl: String? = null) {
Box(modifier = Modifier.fillMaxSize()) {
val cardImageModifier = Modifier
.fillMaxSize()
.align(Alignment.Center)
SubcomposeAsyncImage(
modifier = cardImageModifier.padding(horizontal = TangemTheme.dimens.size34),
model = ImageRequest.Builder(LocalContext.current)
.data(cardImageUrl)
.crossfade(true)
.build(),
loading = { CardPlaceHolder(cardImageModifier) },
error = { CardPlaceHolder(cardImageModifier) },
contentDescription = null,
)
}
}
@Composable
private fun CardPlaceHolder(modifier: Modifier = Modifier) {
Image(
modifier = modifier,
painter = painterResource(id = R.drawable.card_placeholder_black),
contentScale = ContentScale.Fit,
contentDescription = null,
)
}

View file

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

View file

@ -1,196 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.SpacerH16
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.extensions.pluralStringResourceSafe
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.onboarding.R
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 kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
/**
[REDACTED_AUTHOR]
*/
@Composable
fun YourSeedPhraseScreen(state: YourSeedPhraseState, modifier: Modifier = Modifier) {
Box(
modifier = modifier.fillMaxSize(),
) {
Column(
modifier = Modifier
.padding(bottom = TangemTheme.dimens.spacing92)
.verticalScroll(rememberScrollState())
.fillMaxSize(),
) {
SegmentSeedBlock(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing20),
state = state.segmentSeedState,
)
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 = stringResourceSafe(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 = stringResourceSafe(id = R.string.onboarding_seed_generate_title))
SpacerH16()
DescriptionSubTitleText(
text = pluralStringResourceSafe(
id = R.plurals.onboarding_seed_generate_message_words_count,
count = state.selectedSeedType.count,
state.selectedSeedType.count,
),
)
}
}
}
@Composable
private fun SegmentSeedBlock(state: SegmentSeedState, modifier: Modifier = Modifier) {
SegmentedButtons(
modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing76),
config = state.seedSegments,
initialSelectedItem = state.selectedSeedType,
onClick = {
state.onSelectType(it)
},
) {
Text(
text = pluralStringResourceSafe(
id = R.plurals.onboarding_seed_generate_words_count,
count = it.count,
it.count,
),
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing10)
.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)
}
}
}
}
}
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun YourSeedPhraseScreenPreview_Light() {
TangemThemePreview {
YourSeedPhraseScreen(
state = YourSeedPhraseState(
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,
showProgress = false,
onClick = {},
),
),
modifier = Modifier
.fillMaxSize()
.background(color = TangemTheme.colors.background.primary),
)
}
}

View file

@ -1,39 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.res.TangemTheme
/**
[REDACTED_AUTHOR]
*/
@Composable
fun OnboardingActionBlock(
modifier: Modifier = Modifier,
firstActionContent: @Composable (() -> Unit)? = null,
secondActionContent: @Composable (() -> Unit)? = null,
) {
if (firstActionContent == null && secondActionContent == null) return
Box(
modifier = modifier
.padding(horizontal = TangemTheme.dimens.size16)
.padding(
top = TangemTheme.dimens.size8,
bottom = TangemTheme.dimens.size32,
),
) {
Column(
modifier = Modifier.align(Alignment.BottomCenter),
) {
firstActionContent?.invoke()
if (firstActionContent != null && secondActionContent != null) SpacerH12()
secondActionContent?.invoke()
}
}
}

View file

@ -1,64 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui.components
import androidx.annotation.StringRes
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.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
/**
* UI component witch provides single and carousel description for the onboarding process
*/
@Composable
fun OnboardingDescriptionBlock(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
Box(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.size36),
) {
content()
}
}
@Composable
fun Description(@StringRes titleRes: Int, @StringRes subTitleRes: Int) {
Column {
DescriptionTitleText(text = stringResourceSafe(id = titleRes))
SpacerH16()
DescriptionSubTitleText(text = stringResourceSafe(id = subTitleRes))
}
}
@Composable
fun DescriptionTitleText(text: String) {
Text(
text = text,
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
}
@Composable
fun DescriptionSubTitleText(text: String) {
// the text style made similar to TextViewOnboarding.Body
Text(
text = text,
style = TangemTheme.typography.body1.copy(
lineHeight = 20.sp,
letterSpacing = 0.03.sp,
),
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
}

View file

@ -1,77 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState
import com.tangem.feature.onboarding.presentation.wallet2.model.SeedPhraseField
/**
[REDACTED_AUTHOR]
*/
class CheckSeedPhraseStateBuilder {
fun updateTextField(
uiState: OnboardingSeedPhraseState,
field: SeedPhraseField,
textFieldValue: TextFieldValue,
): OnboardingSeedPhraseState = when (field) {
SeedPhraseField.Second -> uiState.copy(
checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
tvSecondPhrase = uiState.checkSeedPhraseState.tvSecondPhrase.copy(
textFieldValue = textFieldValue,
),
),
)
SeedPhraseField.Seventh -> uiState.copy(
checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
tvSeventhPhrase = uiState.checkSeedPhraseState.tvSeventhPhrase.copy(
textFieldValue = textFieldValue,
),
),
)
SeedPhraseField.Eleventh -> uiState.copy(
checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
tvEleventhPhrase = uiState.checkSeedPhraseState.tvEleventhPhrase.copy(
textFieldValue = textFieldValue,
),
),
)
}
fun updateTextFieldError(
uiState: OnboardingSeedPhraseState,
field: SeedPhraseField,
hasError: Boolean,
): OnboardingSeedPhraseState = when (field) {
SeedPhraseField.Second -> uiState.copy(
checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
tvSecondPhrase = uiState.checkSeedPhraseState.tvSecondPhrase.copy(
isError = hasError,
),
),
)
SeedPhraseField.Seventh -> uiState.copy(
checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
tvSeventhPhrase = uiState.checkSeedPhraseState.tvSeventhPhrase.copy(
isError = hasError,
),
),
)
SeedPhraseField.Eleventh -> uiState.copy(
checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
tvEleventhPhrase = uiState.checkSeedPhraseState.tvEleventhPhrase.copy(
isError = hasError,
),
),
)
}
fun updateCreateWalletButton(uiState: OnboardingSeedPhraseState, enabled: Boolean): OnboardingSeedPhraseState {
return uiState.copy(
checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
buttonCreateWallet = uiState.checkSeedPhraseState.buttonCreateWallet.copy(
enabled = enabled,
),
),
)
}
}

View file

@ -1,108 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.feature.onboarding.domain.InsertSuggestionResult
import com.tangem.feature.onboarding.domain.SeedPhraseError
import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState
import com.tangem.feature.onboarding.presentation.wallet2.model.ShowPassphraseInfoBottomSheetContent
import kotlinx.collections.immutable.ImmutableList
/**
[REDACTED_AUTHOR]
*/
class ImportSeedPhraseStateBuilder {
fun updateSeedPhraseTextField(
uiState: OnboardingSeedPhraseState,
textFieldValue: TextFieldValue,
): OnboardingSeedPhraseState = uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
fieldSeedPhrase = uiState.importSeedPhraseState.fieldSeedPhrase.copy(
textFieldValue = textFieldValue,
),
),
)
fun updatePassPhraseTextField(
uiState: OnboardingSeedPhraseState,
textFieldValue: TextFieldValue,
): OnboardingSeedPhraseState = uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
fieldPassphrase = uiState.importSeedPhraseState.fieldPassphrase.copy(
textFieldValue = textFieldValue,
),
),
)
fun updateCreateWalletButton(uiState: OnboardingSeedPhraseState, enabled: Boolean): OnboardingSeedPhraseState {
return uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
buttonCreateWallet = uiState.importSeedPhraseState.buttonCreateWallet.copy(
enabled = enabled,
),
),
)
}
fun updateInvalidWords(uiState: OnboardingSeedPhraseState, invalidWords: Set<String>): OnboardingSeedPhraseState =
uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
invalidWords = invalidWords,
),
)
fun updateSuggestions(
uiState: OnboardingSeedPhraseState,
suggestions: ImmutableList<String>,
): OnboardingSeedPhraseState = uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
suggestionsList = suggestions,
),
)
fun showPassphraseInfoBottomSheet(
uiState: OnboardingSeedPhraseState,
onDismiss: () -> Unit,
): OnboardingSeedPhraseState {
return uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
bottomSheetConfig = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = onDismiss,
content = ShowPassphraseInfoBottomSheetContent(onDismiss),
),
),
)
}
fun dismissPassphraseBottomSheet(uiState: OnboardingSeedPhraseState): OnboardingSeedPhraseState {
return uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
bottomSheetConfig = uiState.importSeedPhraseState.bottomSheetConfig?.copy(
isShown = false,
),
),
)
}
fun insertSuggestionWord(
uiState: OnboardingSeedPhraseState,
insertResult: InsertSuggestionResult,
): OnboardingSeedPhraseState {
val newTextFieldValue = uiState.importSeedPhraseState.fieldSeedPhrase.textFieldValue.copy(
text = insertResult.text,
selection = TextRange(insertResult.cursorPosition, insertResult.cursorPosition),
)
return updateSeedPhraseTextField(uiState, newTextFieldValue)
}
fun updateError(uiState: OnboardingSeedPhraseState, error: SeedPhraseError?): OnboardingSeedPhraseState {
return uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
error = error,
),
)
}
}

View file

@ -1,197 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders
import com.tangem.feature.onboarding.presentation.wallet2.model.*
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
/**
[REDACTED_AUTHOR]
*/
class StateBuilder(
private val uiActions: UiActions,
) {
val checkSeedPhrase: CheckSeedPhraseStateBuilder = CheckSeedPhraseStateBuilder()
val importSeedPhrase: ImportSeedPhraseStateBuilder = ImportSeedPhraseStateBuilder()
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(
fieldSeedPhrase = TextFieldState(
onTextFieldValueChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onTextFieldChanged,
onFocusChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onFocusChanged,
),
fieldPassphrase = TextFieldState(
onTextFieldValueChanged = uiActions.importSeedPhraseActions.passTextFieldAction.onTextFieldChanged,
onFocusChanged = uiActions.importSeedPhraseActions.passTextFieldAction.onFocusChanged,
),
onSuggestedPhraseClick = uiActions.importSeedPhraseActions.suggestedPhraseClick,
onPassphraseInfoClick = uiActions.importSeedPhraseActions.onPassphraseInfoClick,
buttonCreateWallet = ButtonState(
enabled = false,
onClick = uiActions.importSeedPhraseActions.buttonCreateWalletClick,
),
)
}
private fun getInitialCheckSeedPhraseState(): CheckSeedPhraseState {
return CheckSeedPhraseState(
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,
),
buttonCreateWallet = ButtonState(
enabled = false,
onClick = uiActions.checkSeedPhraseActions.buttonCreateWalletClick,
),
)
}
private fun getInitialYourSeedPhraseState(selectedSeedType: SegmentSeedType): YourSeedPhraseState {
return YourSeedPhraseState(
segmentSeedState = SegmentSeedState(
seedSegments = persistentListOf(
SegmentSeedType.SEED_12,
SegmentSeedType.SEED_24,
),
selectedSeedType = selectedSeedType,
onSelectType = uiActions.yourSeedPhraseActions.onSelectType,
),
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(
onClick = uiActions.introActions.buttonCreateWalletClick,
),
buttonOtherOptions = ButtonState(
onClick = uiActions.introActions.buttonOtherOptionsClick,
),
)
}
fun setCardArtwork(uiState: OnboardingSeedPhraseState, cardArtwork: String): OnboardingSeedPhraseState =
uiState.copy(
introState = uiState.introState.copy(
cardImageUrl = cardArtwork,
),
)
fun generateMnemonicComponents(uiState: OnboardingSeedPhraseState): OnboardingSeedPhraseState {
return uiState.copy(
aboutState = uiState.aboutState.copy(
buttonGenerateSeedPhrase = uiState.aboutState.buttonGenerateSeedPhrase.copy(
showProgress = true,
),
buttonImportSeedPhrase = uiState.aboutState.buttonImportSeedPhrase.copy(
enabled = false,
),
),
)
}
fun mnemonicGenerated(
uiState: OnboardingSeedPhraseState,
mnemonicGridItems: PersistentList<MnemonicGridItem>,
): OnboardingSeedPhraseState {
return updateMnemonicComponents(uiState, mnemonicGridItems)
.copy(
aboutState = uiState.aboutState.copy(
buttonGenerateSeedPhrase = uiState.aboutState.buttonGenerateSeedPhrase.copy(
showProgress = false,
),
buttonImportSeedPhrase = uiState.aboutState.buttonImportSeedPhrase.copy(
enabled = true,
),
),
)
}
fun selectSeedType(
uiState: OnboardingSeedPhraseState,
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

@ -1,79 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.viewmodel
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
import com.tangem.utils.extensions.isSingleItem
/**
[REDACTED_AUTHOR]
*/
class InvalidWordsColorTransformation(
private val wordsToBrush: Set<String>,
private val style: SpanStyle,
) : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
val plainText = text.text
if (wordsToBrush.isSingleItem() && wordsToBrush.first() == plainText) {
return plainText.annotate().toTransformedText()
}
val wordList = makeSeparatedWordlist(plainText)
val annotatedString = makeAnnotatedString(wordList)
return annotatedString.toTransformedText()
}
private fun makeSeparatedWordlist(plainText: String): List<String> {
val plainTextCharArray = plainText.toCharArray()
val wordlist = mutableListOf<String>()
var wordBuilder = StringBuilder()
for (index in plainTextCharArray.indices) {
val char = plainTextCharArray[index]
if (char.isWhitespace()) {
if (wordBuilder.isEmpty()) {
wordlist.add(WORD_SEPARATOR)
} else {
wordlist.add(wordBuilder.toString())
wordBuilder = StringBuilder()
wordlist.add(WORD_SEPARATOR)
}
} else {
if (index == plainTextCharArray.lastIndex) {
wordBuilder.append(char)
wordlist.add(wordBuilder.toString())
wordBuilder = StringBuilder()
} else {
wordBuilder.append(char)
}
}
}
return wordlist.toList()
}
private fun makeAnnotatedString(wordList: List<String>): AnnotatedString = buildAnnotatedString {
wordList.forEach {
if (it == WORD_SEPARATOR) {
append(WHITE_SPACE)
} else if (wordsToBrush.contains(it)) {
append(it.annotate())
} else {
append(it)
}
}
}
private fun String.annotate(): AnnotatedString = AnnotatedString(this, style)
private fun AnnotatedString.toTransformedText(): TransformedText = TransformedText(this, OffsetMapping.Identity)
companion object {
private const val WHITE_SPACE = " "
private const val WORD_SEPARATOR = "_$@-*"
}
}

View file

@ -1,28 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.viewmodel
import android.content.Context
import com.tangem.common.module.ModuleMessageConverter
import com.tangem.feature.onboarding.R
import com.tangem.feature.onboarding.domain.SeedPhraseError
/**
[REDACTED_AUTHOR]
*/
class SeedPhraseErrorConverter : ModuleMessageConverter<Pair<Context, SeedPhraseError?>, String?> {
override fun convert(source: Pair<Context, SeedPhraseError?>): String? {
val (context, message) = source
val convertedMessage = when (message) {
SeedPhraseError.InvalidChecksum -> {
context.getString(R.string.onboarding_seed_mnemonic_invalid_checksum)
}
is SeedPhraseError.InvalidWords -> {
context.getString(R.string.onboarding_seed_mnemonic_wrong_words)
}
else -> null
}
return convertedMessage
}
}

View file

@ -1,22 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.viewmodel
import com.tangem.common.CompletionResult
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
/**
[REDACTED_AUTHOR]
*/
interface SeedPhraseMediator {
fun createWallet(callback: (CompletionResult<CreateWalletResponse>) -> Unit)
fun importWallet(
mnemonicComponents: List<String>,
passphrase: String?,
seedPhraseSource: SeedPhraseSource,
callback: (CompletionResult<CreateWalletResponse>) -> Unit,
)
fun onWalletCreated(result: CompletionResult<CreateWalletResponse>)
fun allowScreenshots(allow: Boolean)
}

View file

@ -1,50 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.viewmodel
import android.net.Uri
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/**
[REDACTED_AUTHOR]
*/
class SeedPhraseRouter(
private val onBack: () -> Unit,
private val onOpenChat: () -> Unit,
private val onOpenUriClick: (Uri) -> Unit,
) {
private val _currentScreen = MutableStateFlow(SeedPhraseScreen.Intro)
val currentScreen: StateFlow<SeedPhraseScreen>
get() = _currentScreen
fun navigateBack() {
_currentScreen.value = when (_currentScreen.value) {
SeedPhraseScreen.CheckSeedPhrase -> SeedPhraseScreen.YourSeedPhrase
SeedPhraseScreen.AboutSeedPhrase -> SeedPhraseScreen.Intro
SeedPhraseScreen.ImportSeedPhrase,
SeedPhraseScreen.YourSeedPhrase,
-> SeedPhraseScreen.AboutSeedPhrase
SeedPhraseScreen.Intro -> {
onBack.invoke()
return
}
}
}
fun openScreen(screen: SeedPhraseScreen) {
_currentScreen.value = screen
}
fun openChat() {
onOpenChat.invoke()
}
fun openUri(uri: Uri) {
onOpenUriClick.invoke(uri)
}
}
enum class SeedPhraseScreen {
Intro, AboutSeedPhrase, YourSeedPhrase, CheckSeedPhrase, ImportSeedPhrase
}

View file

@ -1,484 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.viewmodel
import android.net.Uri
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.common.CompletionResult
import com.tangem.core.analytics.api.AnalyticsEventHandler
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 dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import javax.inject.Inject
/**
[REDACTED_AUTHOR]
*/
@Suppress("LargeClass")
@HiltViewModel
class SeedPhraseViewModel @Inject constructor(
private val interactor: SeedPhraseInteractor,
private val localeProvider: LocaleProvider,
private val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
) : ViewModel() {
private var uiBuilder = StateBuilder(uiActions = createUiActions())
private var selectedMnemonicType = MnemonicType.Mnemonic12
var uiState: OnboardingSeedPhraseState by mutableStateOf(uiBuilder.init(fromMnemonicType(selectedMnemonicType)))
private set
private lateinit var router: SeedPhraseRouter
private lateinit var mediator: SeedPhraseMediator
val currentScreen: StateFlow<SeedPhraseScreen>
get() = router.currentScreen
val progress: Flow<Int>
get() = currentScreen.map { progressByScreen[it] ?: 0 }
val maxProgress: Int by lazy { progressByScreen.maxOfOrNull { it.value } ?: 0 }
var isFinished: Boolean = false
private set
private val progressByScreen = mapOf(
SeedPhraseScreen.Intro to 1,
SeedPhraseScreen.AboutSeedPhrase to 2,
SeedPhraseScreen.YourSeedPhrase to 3,
SeedPhraseScreen.CheckSeedPhrase to 3,
SeedPhraseScreen.ImportSeedPhrase to 3,
)
private val textFieldsDebouncers = mutableMapOf<String, Debouncer>()
private var generatedMnemonicComponents: Map<MnemonicType, List<String>> = emptyMap()
private var importedMnemonicComponents: List<String>? = null
private var importedPassphrase: String? = null
override fun onCleared() {
textFieldsDebouncers.forEach { entry -> entry.value.release() }
textFieldsDebouncers.clear()
super.onCleared()
}
fun setRouter(router: SeedPhraseRouter) {
this.router = router
subscribeToScreenChanges()
}
private fun subscribeToScreenChanges() {
router.currentScreen.onEach { screen ->
when (screen) {
SeedPhraseScreen.Intro -> {
/* no-op */
}
SeedPhraseScreen.AboutSeedPhrase -> {
analyticsEventHandler.send(SeedPhraseEvents.IntroScreenOpened)
mediator.allowScreenshots(true)
}
SeedPhraseScreen.YourSeedPhrase -> {
analyticsEventHandler.send(SeedPhraseEvents.GenerationScreenOpened)
mediator.allowScreenshots(false)
}
SeedPhraseScreen.CheckSeedPhrase -> {
analyticsEventHandler.send(SeedPhraseEvents.CheckingScreenOpened)
mediator.allowScreenshots(true)
}
SeedPhraseScreen.ImportSeedPhrase -> {
analyticsEventHandler.send(SeedPhraseEvents.ImportScreenOpened)
}
}
}.launchIn(viewModelScope)
}
fun setMediator(mediator: SeedPhraseMediator) {
this.mediator = mediator
}
fun setCardArtworkUri(cardArtworkUri: String) {
if (uiState.introState.cardImageUrl != cardArtworkUri) {
uiState = uiBuilder.setCardArtwork(uiState, cardArtworkUri)
}
}
private fun createUiActions(): UiActions = UiActions(
introActions = IntroUiAction(
buttonCreateWalletClick = ::buttonCreateWalletClick,
buttonOtherOptionsClick = ::buttonOtherOptionsClick,
),
aboutActions = AboutUiAction(
buttonReadMoreAboutSeedPhraseClick = ::buttonReadMoreAboutSeedPhraseClick,
buttonGenerateSeedPhraseClick = ::buttonGenerateSeedPhraseClick,
buttonImportSeedPhraseClick = ::buttonImportSeedPhraseClick,
),
yourSeedPhraseActions = YourSeedPhraseUiAction(
buttonContinueClick = ::buttonContinueClick,
onSelectType = ::onSelectType,
),
checkSeedPhraseActions = CheckSeedPhraseUiAction(
buttonCreateWalletClick = {
val mnemonicComponents =
generatedMnemonicComponents[selectedMnemonicType] ?: return@CheckSeedPhraseUiAction
buttonImportWalletClick(mnemonicComponents, null, SeedPhraseSource.GENERATED)
},
secondTextFieldAction = TextFieldUiAction(
onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Second, value) },
),
seventhTextFieldAction = TextFieldUiAction(
onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Seventh, value) },
),
eleventhTextFieldAction = TextFieldUiAction(
onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Eleventh, value) },
),
),
importSeedPhraseActions = ImportSeedPhraseUiAction(
phraseTextFieldAction = TextFieldUiAction(
onTextFieldChanged = { value -> onSeedPhraseTextFieldChanged(value) },
),
passTextFieldAction = TextFieldUiAction(
onTextFieldChanged = { value -> onPassPhraseTextFieldChanged(value) },
),
suggestedPhraseClick = ::buttonSuggestedPhraseClick,
onPassphraseInfoClick = ::buttonPassphraseInfoClick,
buttonCreateWalletClick = {
analyticsEventHandler.send(SeedPhraseEvents.ButtonImport)
buttonImportWalletClick(importedMnemonicComponents, importedPassphrase, SeedPhraseSource.IMPORTED)
},
),
menuChatClick = ::menuChatClick,
menuNavigateBackClick = ::menuNavigateBackClick,
)
// region CheckSeedPhrase
private fun onTextFieldChanged(field: SeedPhraseField, textFieldValue: TextFieldValue) {
launchSingle {
updateUi { uiBuilder.checkSeedPhrase.updateTextField(uiState, field, textFieldValue) }
val fieldState = field.getState(uiState)
if (fieldState.textFieldValue.text.isEmpty()) {
updateUi {
val mediate = uiBuilder.checkSeedPhrase.updateTextFieldError(uiState, field, hasError = false)
uiBuilder.checkSeedPhrase.updateCreateWalletButton(mediate, enabled = false)
}
return@launchSingle
}
createOrGetDebouncer(field.name).debounce(viewModelScope, context = dispatchers.io) {
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.entries
.map { field -> field.getState(uiState) }
.all { fieldState -> fieldState.textFieldValue.text.isNotEmpty() && !fieldState.isError }
if (uiState.checkSeedPhraseState.buttonCreateWallet.enabled != isCreateWalletButtonEnabled) {
updateUi {
uiBuilder.checkSeedPhrase.updateCreateWalletButton(
uiState = uiState,
enabled = isCreateWalletButtonEnabled,
)
}
}
}
}
}
// endregion CheckSeedPhrase
// region ImportSeedPhrase
private fun onSeedPhraseTextFieldChanged(textFieldValue: TextFieldValue) {
val oldTextFieldValue = uiState.importSeedPhraseState.fieldSeedPhrase.textFieldValue
val isSameText = textFieldValue.text == oldTextFieldValue.text
val isCursorMoved = textFieldValue.selection != oldTextFieldValue.selection
uiState = uiBuilder.importSeedPhrase.updateSeedPhraseTextField(uiState, textFieldValue)
val fieldState = uiState.importSeedPhraseState.fieldSeedPhrase
val inputMnemonic = fieldState.textFieldValue.text
val debouncer = createOrGetDebouncer(MNEMONIC_DEBOUNCER)
when {
isSameText && isCursorMoved -> {
debouncer.debounce(viewModelScope, MNEMONIC_DEBOUNCE_DELAY, dispatchers.single) {
updateSuggestions(fieldState)
}
}
isSameText && !isCursorMoved -> {
// do nothing
}
else -> {
debouncer.debounce(viewModelScope, MNEMONIC_DEBOUNCE_DELAY, dispatchers.single) {
updateSuggestions(fieldState)
validateMnemonic(inputMnemonic)
}
}
}
}
private fun onPassPhraseTextFieldChanged(textFieldValue: TextFieldValue) {
importedPassphrase = textFieldValue.text
uiState = uiBuilder.importSeedPhrase.updatePassPhraseTextField(uiState, textFieldValue)
}
private suspend fun validateMnemonic(inputMnemonic: String) {
if (inputMnemonic.isEmpty() && uiState.importSeedPhraseState.error != null) {
val mediateState = uiBuilder.importSeedPhrase.updateCreateWalletButton(uiState, enabled = false)
updateUi {
uiBuilder.importSeedPhrase.updateError(mediateState, null)
}
return
}
interactor.validateMnemonicString(inputMnemonic)
.onSuccess { mnemonicComponents ->
importedMnemonicComponents = mnemonicComponents
val mediateState = uiBuilder.importSeedPhrase.updateCreateWalletButton(uiState, enabled = true)
updateUi { uiBuilder.importSeedPhrase.updateError(mediateState, null) }
}
.onFailure {
uiState = uiBuilder.importSeedPhrase.updateCreateWalletButton(uiState, enabled = false)
val error = it as? SeedPhraseError ?: return
importedMnemonicComponents = null
val mediateState = when (error) {
is SeedPhraseError.InvalidWords -> {
uiBuilder.importSeedPhrase.updateInvalidWords(uiState, error.words)
}
else -> uiState
}
updateUi { uiBuilder.importSeedPhrase.updateError(mediateState, error) }
}
}
private suspend fun updateSuggestions(fieldState: TextFieldState) {
val suggestions = interactor.getSuggestions(
text = fieldState.textFieldValue.text,
hasSelection = !fieldState.textFieldValue.selection.collapsed,
cursorPosition = fieldState.textFieldValue.selection.end,
)
updateUi { uiBuilder.importSeedPhrase.updateSuggestions(uiState, suggestions) }
}
// endregion ImportSeedPhrase
// region ButtonClickHandlers
private fun buttonCreateWalletClick() {
viewModelScope.launch(dispatchers.io) {
analyticsEventHandler.send(CreateWalletEvents.OnboardingSeedButtonCreateWallet)
mediator.createWallet(::handleWalletCreationResult)
}
}
private fun buttonImportWalletClick(
mnemonicComponents: List<String>?,
passphrase: String?,
seedPhraseSource: SeedPhraseSource,
) {
mnemonicComponents ?: return
viewModelScope.launch(dispatchers.io) {
mediator.importWallet(mnemonicComponents, passphrase, seedPhraseSource, ::handleWalletCreationResult)
}
}
private fun handleWalletCreationResult(result: CompletionResult<CreateWalletResponse>) {
when (result) {
is CompletionResult.Success -> {
isFinished = true
}
is CompletionResult.Failure -> {
// errors shows on the TangemSdk bottom sheet dialog
}
}
mediator.onWalletCreated(result)
}
private fun menuNavigateBackClick() {
router.navigateBack()
}
private fun menuChatClick() {
router.openChat()
}
private fun buttonOtherOptionsClick() {
analyticsEventHandler.send(CreateWalletEvents.OnboardingSeedButtonOtherOptions)
router.openScreen(SeedPhraseScreen.AboutSeedPhrase)
}
private fun buttonReadMoreAboutSeedPhraseClick() {
analyticsEventHandler.send(SeedPhraseEvents.ButtonReadMore)
val webUri = Uri.Builder()
.scheme("https")
.authority("tangem.com")
.appendPath(localeProvider.getWebUriLocaleLanguage())
.appendPath("blog/post/seed-phrase-a-risky-solution")
.build()
router.openUri(webUri)
}
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.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.toPersistentList())
}
}
.onFailure {
generatedMnemonicComponents = emptyMap()
// TODO: show error
}
}
}
private fun toMnemonicGridList(mnemonicComponents: List<String>): PersistentList<MnemonicGridItem> {
return mnemonicComponents.mapIndexed { index, word ->
MnemonicGridItem(
index = index.inc(),
mnemonic = word,
)
}.toPersistentList()
}
private fun buttonImportSeedPhraseClick() {
analyticsEventHandler.send(SeedPhraseEvents.ButtonImportWallet)
router.openScreen(SeedPhraseScreen.ImportSeedPhrase)
}
private fun buttonContinueClick() {
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) {
launchSingle {
val textFieldValue = uiState.importSeedPhraseState.fieldSeedPhrase.textFieldValue
val word = uiState.importSeedPhraseState.suggestionsList[suggestionIndex]
val cursorPosition = textFieldValue.selection.end
val insertResult = interactor.insertSuggestionWord(
text = textFieldValue.text,
suggestion = word,
cursorPosition = cursorPosition,
)
updateUi {
val mediateState = uiBuilder.importSeedPhrase.insertSuggestionWord(uiState, insertResult)
uiBuilder.importSeedPhrase.updateSuggestions(mediateState, persistentListOf())
// copy of TextFieldValue has no effect for textChange callback and seed validation doesn't work
// call validation force
}
validateMnemonic(uiState.importSeedPhraseState.fieldSeedPhrase.textFieldValue.text)
}
}
private fun buttonPassphraseInfoClick() {
launchSingle {
updateUi {
uiBuilder.importSeedPhrase.showPassphraseInfoBottomSheet(uiState) {
dismissPassphraseBottomSheet()
}
}
}
}
private fun dismissPassphraseBottomSheet() {
launchSingle {
updateUi { uiBuilder.importSeedPhrase.dismissPassphraseBottomSheet(uiState) }
}
}
// endregion ButtonClickHandlers
// region Utils
/**
* Updating the UI with a contract where all copying of objects are called in the Single thread context and
* updating the UiState in the main context.
*/
private suspend fun updateUi(updateBlock: suspend () -> OnboardingSeedPhraseState) {
withSingleContext {
val newState = updateBlock.invoke()
withMainContext { uiState = newState }
}
}
private fun createOrGetDebouncer(name: String): Debouncer {
return textFieldsDebouncers[name] ?: Debouncer().apply { textFieldsDebouncers[name] = this }
}
private fun SeedPhraseField.getState(uiState: OnboardingSeedPhraseState): TextFieldState = when (this) {
SeedPhraseField.Second -> uiState.checkSeedPhraseState.tvSecondPhrase
SeedPhraseField.Seventh -> uiState.checkSeedPhraseState.tvSeventhPhrase
SeedPhraseField.Eleventh -> uiState.checkSeedPhraseState.tvEleventhPhrase
}
private fun launchSingle(block: suspend CoroutineScope.() -> Unit): Job {
return viewModelScope.launch(dispatchers.single, block = block)
}
private suspend fun <T> withMainContext(block: suspend CoroutineScope.() -> T): T {
return withContext(dispatchers.main, block)
}
private suspend fun <T> withSingleContext(block: suspend CoroutineScope.() -> T): T {
return withContext(dispatchers.single, block)
}
// endregion Utils
private companion object {
const val MNEMONIC_DEBOUNCER = "MnemonicDebouncer"
const val MNEMONIC_DEBOUNCE_DELAY = 700L
const val DELAY_GENERATE_SEED_PHRASE = 300L
}
}

View file

@ -19,8 +19,6 @@ import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.LegacyAction
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
@ -34,7 +32,6 @@ import com.tangem.feature.walletsettings.entity.WalletSettingsUM
import com.tangem.feature.walletsettings.impl.R
import com.tangem.feature.walletsettings.utils.ItemsBuilder
import com.tangem.features.nft.NFTFeatureToggles
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
@ -55,12 +52,10 @@ internal class WalletSettingsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
private val analyticsContextProxy: AnalyticsContextProxy,
private val reduxStateHolder: ReduxStateHolder,
private val getShouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val walletsRepository: WalletsRepository,
private val nftFeatureToggles: NFTFeatureToggles,
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
) : Model() {
val params: WalletSettingsComponent.Params = paramsContainer.require()
@ -163,23 +158,12 @@ internal class WalletSettingsModel @Inject constructor(
analyticsEventHandler.send(Settings.ButtonCreateBackup)
analyticsContextProxy.addContext(scanResponse)
if (onboardingV2FeatureToggles.isOnboardingV2Enabled) {
router.push(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = AppRoute.Onboarding.Mode.AddBackupWallet1,
),
)
} else {
reduxStateHolder.dispatch(
LegacyAction.StartOnboardingProcess(
scanResponse = scanResponse,
canSkipBackup = false,
),
)
router.push(AppRoute.OnboardingWallet())
}
router.push(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = AppRoute.Onboarding.Mode.AddBackupWallet1,
),
)
}
private fun onCheckedNFTChange(isChecked: Boolean) {

View file

@ -96,9 +96,6 @@ dependencies {
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
//TODO: Create api/impl modules for onboarding [REDACTED_JIRA]
implementation(projects.features.onboarding)
/** Feature Apis */
implementation(projects.features.details.api)
implementation(projects.features.manageTokens.api)

View file

@ -14,8 +14,6 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.promo.ShouldShowSwapPromoWalletUseCase
import com.tangem.domain.redux.LegacyAction
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.NeverToSuggestRateAppUseCase
import com.tangem.domain.settings.RemindToRateAppLaterUseCase
import com.tangem.domain.tokens.FetchTokenListUseCase
@ -96,7 +94,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
private val scanCardToUnlockWalletClickHandler: ScanCardToUnlockWalletClickHandler,
private val unlockWalletsUseCase: UnlockWalletsUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val reduxStateHolder: ReduxStateHolder,
private val dispatchers: CoroutineDispatcherProvider,
private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase,
private val getCardInfoUseCase: GetCardInfoUseCase,
@ -116,14 +113,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
private fun prepareAndStartOnboardingProcess() {
modelScope.launch(dispatchers.main) {
getSelectedUserWallet()?.let {
reduxStateHolder.dispatch(
LegacyAction.StartOnboardingProcess(
scanResponse = it.scanResponse,
canSkipBackup = false,
),
)
// navigation action shouldn't be out of coroutine to avoid race
router.openOnboardingScreen(
scanResponse = it.scanResponse,
continueBackup = true,

View file

@ -15,7 +15,6 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.navigation.WalletRoute
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig
import com.tangem.features.biometry.BiometryFeatureToggles
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import javax.inject.Inject
@ -26,7 +25,6 @@ internal class DefaultWalletRouter @Inject constructor(
private val router: AppRouter,
private val urlOpener: UrlOpener,
private val reduxStateHolder: ReduxStateHolder,
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
private val biometryFeatureToggles: BiometryFeatureToggles,
) : InnerWalletRouter {
@ -50,22 +48,16 @@ internal class DefaultWalletRouter @Inject constructor(
}
override fun openOnboardingScreen(scanResponse: ScanResponse, continueBackup: Boolean) {
if (onboardingV2FeatureToggles.isOnboardingV2Enabled) {
router.push(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = if (continueBackup) {
AppRoute.Onboarding.Mode.AddBackupWallet1
} else {
AppRoute.Onboarding.Mode.Onboarding
},
),
)
} else {
router.push(
AppRoute.OnboardingWallet(canSkipBackup = false),
)
}
router.push(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = if (continueBackup) {
AppRoute.Onboarding.Mode.AddBackupWallet1
} else {
AppRoute.Onboarding.Mode.Onboarding
},
),
)
}
override fun openOnrampSuccessScreen(txId: String) {