Updated on 2026-08-14
This commit is contained in:
parent
cf05b371ef
commit
e080401183
103 changed files with 142 additions and 10555 deletions
|
|
@ -1,8 +1,5 @@
|
|||
package com.tangem.features.onboarding.v2
|
||||
|
||||
interface OnboardingV2FeatureToggles {
|
||||
val isOnboardingV2Enabled: Boolean
|
||||
val isVisaOnboardingEnabled: Boolean
|
||||
val isNoteRefactoringEnabled: Boolean
|
||||
val isTwinRefactoringEnabled: Boolean
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
1
features/onboarding/.gitignore
vendored
1
features/onboarding/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
/build
|
||||
|
|
@ -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)
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package com.tangem.feature.onboarding.domain.models
|
||||
|
||||
enum class MnemonicType {
|
||||
Mnemonic12, Mnemonic24;
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
package com.tangem.feature.onboarding.navigation
|
||||
|
||||
class DefaultOnboardingRouter : OnboardingRouter
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package com.tangem.feature.onboarding.navigation
|
||||
|
||||
/**
|
||||
* Onboarding router
|
||||
*/
|
||||
// TODO: Move to onboarding api module [REDACTED_JIRA]
|
||||
interface OnboardingRouter
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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 = {},
|
||||
)
|
||||
|
|
@ -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),
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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 { })
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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 = "_$@-*"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue