Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-21 17:49:53 +03:00
parent 5a6dfd50e4
commit c6497a0abc
19 changed files with 448 additions and 54 deletions

View file

@ -4,8 +4,36 @@
<inspection_tool class="CanBeParameter" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="CanBeParameter" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="CanBePrimaryConstructorProperty" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="CanBePrimaryConstructorProperty" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="ComplexRedundantLet" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="ComplexRedundantLet" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ConstantConditionIf" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="ConstantConditionIf" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="ExplicitThis" enabled="false" level="ERROR" enabled_by_default="false" /> <inspection_tool class="ExplicitThis" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="KotlinDoubleNegation" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="KotlinDoubleNegation" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="KotlinRedundantOverride" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="KotlinRedundantOverride" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="KotlinUnusedImport" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="KotlinUnusedImport" enabled="true" level="ERROR" enabled_by_default="true" />
@ -19,6 +47,10 @@
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" /> <option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true"> <inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" /> <option name="previewFile" value="true" />

View file

@ -25,6 +25,9 @@ dependencies {
implementation(projects.core.decompose) implementation(projects.core.decompose)
implementation(projects.core.navigation) implementation(projects.core.navigation)
/** Common */
implementation(projects.common.ui)
/** Domain */ /** Domain */
implementation(projects.domain.models) implementation(projects.domain.models)
implementation(projects.domain.feedback) implementation(projects.domain.feedback)
@ -44,7 +47,7 @@ dependencies {
implementation(deps.lifecycle.runtime.ktx) implementation(deps.lifecycle.runtime.ktx)
/** Compose libraries */ /** Compose libraries */
implementation(deps.compose.material) // to use buttons implementation(deps.compose.material) // to use buttons and text field in MultiWalletSeedPhraseImport.kt
implementation(deps.compose.material3) implementation(deps.compose.material3)
implementation(deps.compose.animation) implementation(deps.compose.animation)
implementation(deps.compose.foundation) implementation(deps.compose.foundation)

View file

@ -15,7 +15,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class MultiWalletCreateWalletComponent( internal class MultiWalletCreateWalletComponent(
context: AppComponentContext, context: AppComponentContext,
params: MultiWalletChildParams, params: MultiWalletChildParams,
onNextStep: (OnboardingMultiWalletState.Step) -> Unit, onNextStep: (OnboardingMultiWalletState.Step) -> Unit,

View file

@ -13,6 +13,7 @@ import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state.MultiWalletCreateWalletUM import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state.MultiWalletCreateWalletUM
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.resetCardDialog
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -24,7 +25,7 @@ import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ComponentScoped @ComponentScoped
class MultiWalletCreateWalletModel @Inject constructor( internal class MultiWalletCreateWalletModel @Inject constructor(
paramsContainer: ParamsContainer, paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
private val tangemSdkManager: TangemSdkManager, private val tangemSdkManager: TangemSdkManager,

View file

@ -21,14 +21,14 @@ import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state.MultiWalletCreateWalletUM import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state.MultiWalletCreateWalletUM
@Composable @Composable
fun MultiWalletCreateWallet(state: MultiWalletCreateWalletUM, modifier: Modifier = Modifier) { internal fun MultiWalletCreateWallet(state: MultiWalletCreateWalletUM, modifier: Modifier = Modifier) {
if (state.dialog != null) { if (state.dialog != null) {
BasicDialog( BasicDialog(
title = state.dialog.title.resolveReference(), title = state.dialog.title.resolveReference(),
message = state.dialog.description.resolveReference(), message = state.dialog.message.resolveReference(),
confirmButton = DialogButtonUM( confirmButton = DialogButtonUM(
title = state.dialog.confirmButtonText.resolveReference(), title = state.dialog.confirmButtonText.resolveReference(),
onClick = state.dialog.onConfirm, onClick = state.dialog.onConfirmClick,
), ),
dismissButton = DialogButtonUM( dismissButton = DialogButtonUM(
title = state.dialog.dismissButtonText.resolveReference(), title = state.dialog.dismissButtonText.resolveReference(),

View file

@ -1,23 +1,13 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state package com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM
data class MultiWalletCreateWalletUM( internal data class MultiWalletCreateWalletUM(
val title: TextReference, val title: TextReference,
val bodyText: TextReference, val bodyText: TextReference,
val showOtherOptionsButton: Boolean, val showOtherOptionsButton: Boolean,
val onCreateWalletClick: () -> Unit, val onCreateWalletClick: () -> Unit,
val onOtherOptionsClick: () -> Unit, val onOtherOptionsClick: () -> Unit,
val dialog: Dialog?, val dialog: OnboardingDialogUM?,
) { )
data class Dialog(
val title: TextReference,
val description: TextReference,
val dismissButtonText: TextReference,
val confirmButtonText: TextReference,
val dismissWarningColor: Boolean = false,
val onConfirm: () -> Unit,
val onDismissButtonClick: () -> Unit,
val onDismiss: () -> Unit,
)
}

View file

@ -1,9 +1,15 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.view.WindowManager
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.model.getOrCreateModel
@ -16,20 +22,24 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Suppress("UnusedPrivateMember") @Suppress("UnusedPrivateMember")
class MultiWalletSeedPhraseComponent( internal class MultiWalletSeedPhraseComponent(
context: AppComponentContext, context: AppComponentContext,
params: MultiWalletChildParams, params: MultiWalletChildParams,
onNextStep: (OnboardingMultiWalletState.Step) -> Unit, onNextStep: (OnboardingMultiWalletState.Step) -> Unit,
backButtonClickFlow: SharedFlow<Unit>, backButtonClickFlow: SharedFlow<Unit>,
) : AppComponentContext by context, MultiWalletChildComponent { ) : AppComponentContext by context, MultiWalletChildComponent {
private val model: MultiWalletSeedPhraseModel = getOrCreateModel() private val model: MultiWalletSeedPhraseModel = getOrCreateModel(params)
init { init {
componentScope.launch { componentScope.launch {
backButtonClickFlow.collect { backButtonClickFlow.collect { model.onBack() }
model.onBack() }
} componentScope.launch {
model.onDone.collect { onNextStep(OnboardingMultiWalletState.Step.AddBackupDevice) }
}
componentScope.launch {
model.navigateBack.collect { onNextStep(OnboardingMultiWalletState.Step.CreateWallet) }
} }
} }
@ -41,9 +51,30 @@ class MultiWalletSeedPhraseComponent(
model.onBack() model.onBack()
} }
// disable screenshots
val activity = LocalContext.current.findActivity()
DisposableEffect(activity) {
activity.window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE,
)
onDispose {
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
MultiWalletSeedPhrase( MultiWalletSeedPhrase(
modifier = modifier, modifier = modifier,
state = state, state = state,
) )
} }
private fun Context.findActivity(): Activity {
var context = this
while (context is ContextWrapper) {
if (context is Activity) return context
context = context.baseContext
}
error("")
}
} }

View file

@ -1,46 +1,78 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder.GenerateSeedPhraseUiStateBuilder import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder.GenerateSeedPhraseUiStateBuilder
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder.ImportSeedPhraseUiStateBuilder
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder.SeedPhraseCheckUiStateBuilder import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder.SeedPhraseCheckUiStateBuilder
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.resetCardDialog
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@Suppress("LongParameterList")
@ComponentScoped @ComponentScoped
class MultiWalletSeedPhraseModel @Inject constructor( internal class MultiWalletSeedPhraseModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
private val mnemonicRepository: MnemonicRepository, private val mnemonicRepository: MnemonicRepository,
private val urlOpener: UrlOpener, private val urlOpener: UrlOpener,
private val tangemSdkManager: TangemSdkManager,
private val cardRepository: CardRepository,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
) : Model() { ) : Model() {
private val params = paramsContainer.require<MultiWalletChildParams>()
private val multiWalletState get() = params.multiWalletState
private val state = MutableStateFlow(SeedPhraseState()) private val state = MutableStateFlow(SeedPhraseState())
private val _uiState = MutableStateFlow(getStartState()) private val _uiState = MutableStateFlow(getStartState())
private val generateSeedPhraseUiStateBuilder = GenerateSeedPhraseUiStateBuilder( private val generateSeedPhraseUiStateBuilder = GenerateSeedPhraseUiStateBuilder(
state = state, state = state,
updateUiState = { block -> updateUiStateSpecific(block) }, updateUiState = { block -> updateUiStateSpecific(block) },
onContinue = { openWordsCheck() }, onContinue = ::openWordsCheck,
) )
private val seedPhraseCheckUiStateBuilder = SeedPhraseCheckUiStateBuilder( private val seedPhraseCheckUiStateBuilder = SeedPhraseCheckUiStateBuilder(
state = state, state = state,
currentUiState = { _uiState.value }, currentUiState = { _uiState.value },
updateUiState = { block -> updateUiStateSpecific(block) }, updateUiState = { block -> updateUiStateSpecific(block) },
importWallet = { }, importWallet = ::importWallet,
)
private val importSeedPhraseUiStateBuilder = ImportSeedPhraseUiStateBuilder(
modelScope = modelScope,
state = state,
mnemonicRepository = mnemonicRepository,
updateUiState = { block -> updateUiStateSpecific(block) },
importWallet = ::importWallet,
) )
val uiState = _uiState.asStateFlow() val uiState = _uiState.asStateFlow()
val onDone = MutableSharedFlow<Unit>()
val navigateBack = MutableSharedFlow<Unit>()
fun onBack() { fun onBack() {
when (_uiState.value) { when (_uiState.value) {
is MultiWalletSeedPhraseUM.Start -> { is MultiWalletSeedPhraseUM.Start -> {
// TODO navigate back modelScope.launch { navigateBack.emit(Unit) }
} }
is MultiWalletSeedPhraseUM.Import, is MultiWalletSeedPhraseUM.Import,
is MultiWalletSeedPhraseUM.GenerateSeedPhrase, is MultiWalletSeedPhraseUM.GenerateSeedPhrase,
@ -56,7 +88,9 @@ class MultiWalletSeedPhraseModel @Inject constructor(
private fun getStartState(): MultiWalletSeedPhraseUM { private fun getStartState(): MultiWalletSeedPhraseUM {
return MultiWalletSeedPhraseUM.Start( return MultiWalletSeedPhraseUM.Start(
onImportSeedPhraseClicked = {}, onImportSeedPhraseClicked = {
openImportSeedPhrase()
},
onGenerateSeedPhraseClicked = { onGenerateSeedPhraseClicked = {
generateSeedPhrase() generateSeedPhrase()
openGeneratedSeedPhrase() openGeneratedSeedPhrase()
@ -73,8 +107,6 @@ class MultiWalletSeedPhraseModel @Inject constructor(
val words12 = state.value.generatedWords12 ?: return val words12 = state.value.generatedWords12 ?: return
val words24 = state.value.generatedWords24 ?: return val words24 = state.value.generatedWords24 ?: return
// TODO disallow screenshots
_uiState.value = generateSeedPhraseUiStateBuilder.getState( _uiState.value = generateSeedPhraseUiStateBuilder.getState(
generatedWords12 = words12, generatedWords12 = words12,
generatedWords24 = words24, generatedWords24 = words24,
@ -85,6 +117,10 @@ class MultiWalletSeedPhraseModel @Inject constructor(
_uiState.value = seedPhraseCheckUiStateBuilder.getState() _uiState.value = seedPhraseCheckUiStateBuilder.getState()
} }
private fun openImportSeedPhrase() {
_uiState.value = importSeedPhraseUiStateBuilder.getState()
}
private fun generateSeedPhrase() { private fun generateSeedPhrase() {
val words12 = mnemonicRepository.generateMnemonic(MnemonicRepository.MnemonicType.Words12) val words12 = mnemonicRepository.generateMnemonic(MnemonicRepository.MnemonicType.Words12)
val words24 = mnemonicRepository.generateMnemonic(MnemonicRepository.MnemonicType.Words24) val words24 = mnemonicRepository.generateMnemonic(MnemonicRepository.MnemonicType.Words24)
@ -97,6 +133,79 @@ class MultiWalletSeedPhraseModel @Inject constructor(
} }
} }
private fun importWallet(shouldReset: Boolean = false) {
// TODO make stateless [REDACTED_TASK_KEY]
val currentState = state.value
val mnemonic = currentState.importedMnemonic
?: (if (currentState.words24Option) currentState.generatedWords24 else currentState.generatedWords12)
?: return
val scanResponse = params.parentParams.scanResponse
modelScope.launch {
val result = tangemSdkManager.importWallet(
scanResponse = scanResponse,
mnemonic = mnemonic.mnemonicComponents.joinToString(" "),
passphrase = currentState.passphrase,
shouldReset = shouldReset,
)
when (result) {
is CompletionResult.Success -> {
// TODO on success send analytics event
multiWalletState.update {
it.copy(
currentScanResponse = it.currentScanResponse.copy(
card = result.data.card,
derivedKeys = result.data.derivedKeys,
primaryCard = result.data.primaryCard,
),
)
}
cardRepository.startCardActivation(cardId = result.data.card.cardId)
onDone.emit(Unit)
}
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.WalletAlreadyCreated) {
// show should reset dialog
handleActivationError()
}
}
}
}
}
private fun handleActivationError() {
updateDialog(
resetCardDialog(
onConfirm = ::navigateToSupportScreen,
dismiss = { updateDialog(null) },
onDismissButtonClick = ::resetCard,
),
)
}
private fun resetCard() = importWallet(true)
fun navigateToSupportScreen() {
modelScope.launch {
val cardInfo = getCardInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch
sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo))
}
}
private fun updateDialog(dialog: OnboardingDialogUM?) {
_uiState.update { st ->
when (st) {
is MultiWalletSeedPhraseUM.GeneratedWordsCheck -> st.copy(dialog = dialog)
is MultiWalletSeedPhraseUM.Import -> st.copy(dialog = dialog)
else -> st
}
}
}
private inline fun <reified T : MultiWalletSeedPhraseUM> updateUiStateSpecific(block: (T) -> T) { private inline fun <reified T : MultiWalletSeedPhraseUM> updateUiStateSpecific(block: (T) -> T) {
_uiState.update { _uiState.update {
if (it !is T) return@update it if (it !is T) return@update it

View file

@ -6,5 +6,7 @@ data class SeedPhraseState(
val generatedWords12: Mnemonic? = null, val generatedWords12: Mnemonic? = null,
val generatedWords24: Mnemonic? = null, val generatedWords24: Mnemonic? = null,
val words24Option: Boolean = false, val words24Option: Boolean = false,
val importedMnemonic: Mnemonic? = null,
val passphrase: String? = null,
val readyToImport: Boolean = false, val readyToImport: Boolean = false,
) )

View file

@ -8,7 +8,7 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
class GenerateSeedPhraseUiStateBuilder( internal class GenerateSeedPhraseUiStateBuilder(
private val state: MutableStateFlow<SeedPhraseState>, private val state: MutableStateFlow<SeedPhraseState>,
private val updateUiState: ( private val updateUiState: (
(MultiWalletSeedPhraseUM.GenerateSeedPhrase) -> MultiWalletSeedPhraseUM.GenerateSeedPhrase, (MultiWalletSeedPhraseUM.GenerateSeedPhrase) -> MultiWalletSeedPhraseUM.GenerateSeedPhrase,

View file

@ -0,0 +1,163 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.common.core.TangemSdkError
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.crypto.bip39.MnemonicErrorResult
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.MnemonicRepository
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.SeedPhraseState
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
internal class ImportSeedPhraseUiStateBuilder(
private val modelScope: CoroutineScope,
private val state: MutableStateFlow<SeedPhraseState>,
private val mnemonicRepository: MnemonicRepository,
private val updateUiState: ((MultiWalletSeedPhraseUM.Import) -> MultiWalletSeedPhraseUM.Import) -> Unit,
private val importWallet: () -> Unit,
) {
private val wordsCheckJobHolder = JobHolder()
fun getState(): MultiWalletSeedPhraseUM {
return MultiWalletSeedPhraseUM.Import(
wordsChange = {
launchInterceptWords(wordsField = it)
suggestNextWord(it)
updateUiState { state ->
state.copy(words = it)
}
},
passPhraseChange = {
state.update { st -> st.copy(passphrase = it.text) }
updateUiState { state -> state.copy(passPhrase = it) }
},
onPassphraseInfoClick = ::showInfoBS,
createWalletClick = importWallet,
onSuggestionClick = { word -> addSuggestedWord(word) },
)
}
private fun addSuggestedWord(word: String) {
updateUiState { st ->
val text = st.words.text
val wordsFromText = text.split(" ").filter { it.isNotBlank() }.map { it.trim() }
val newWords = wordsFromText.dropLast(1) + word
val newWordsText = newWords.joinToString(" ")
st.copy(
words = TextFieldValue(
text = newWordsText,
selection = TextRange(newWordsText.length),
),
)
}
}
private fun suggestNextWord(wordsField: TextFieldValue) {
if (wordsField.text.endsWith(" ")) {
updateUiState { it.copy(suggestionsList = emptyList<String>().toImmutableList()) }
return
}
val text = wordsField.text
val wordsFromText = text.split(" ").filter { it.isNotBlank() }.map { it.trim() }
val lastWord = wordsFromText.lastOrNull() ?: return
if (lastWord.length < 2) return
if (mnemonicRepository.words.contains(lastWord) && mnemonicRepository.words.none { it.startsWith(lastWord) }) {
updateUiState { it.copy(suggestionsList = emptyList<String>().toImmutableList()) }
return
}
updateUiState { st ->
st.copy(
suggestionsList = mnemonicRepository.words
.filter { it.startsWith(lastWord) && it != lastWord }
.toImmutableList(),
)
}
}
private fun launchInterceptWords(wordsField: TextFieldValue) {
modelScope.launch {
delay(timeMillis = 500)
interceptWords(wordsField)
}.saveIn(wordsCheckJobHolder)
}
private fun interceptWords(wordsField: TextFieldValue) {
updateUiState {
it.copy(
createWalletEnabled = false,
wordsErrorText = null,
)
}
state.value = state.value.copy(importedMnemonic = null)
val text = wordsField.text
val wordsFromText = text.split(" ").filter { it.isNotBlank() }.map { it.trim() }
val invalidWords = wordsFromText.filterNot { it in mnemonicRepository.words }
if (invalidWords.isNotEmpty()) {
updateUiState {
it.copy(
invalidWords = invalidWords.toImmutableList(),
wordsErrorText = resourceReference(R.string.onboarding_seed_mnemonic_wrong_words),
createWalletEnabled = false,
)
}
return
}
try {
val mnemonic = mnemonicRepository.generateMnemonic(text)
state.value = state.value.copy(importedMnemonic = mnemonic)
updateUiState {
it.copy(
invalidWords = emptyList<String>().toImmutableList(),
wordsErrorText = null,
createWalletEnabled = true,
)
}
} catch (ex: TangemSdkError.MnemonicException) {
val error = ex.mnemonicResult
if (error is MnemonicErrorResult.InvalidChecksum) {
updateUiState {
it.copy(
wordsErrorText = resourceReference(R.string.onboarding_seed_mnemonic_invalid_checksum),
createWalletEnabled = false,
)
}
} else {
updateUiState {
it.copy(
createWalletEnabled = false,
wordsErrorText = null,
)
}
}
}
}
private fun showInfoBS() {
updateUiState { state ->
state.copy(
infoBottomSheetConfig = TangemBottomSheetConfig.Empty.copy(
isShow = true,
onDismissRequest = {
updateUiState { it.copy(infoBottomSheetConfig = TangemBottomSheetConfig.Empty) }
},
),
)
}
}
}

View file

@ -7,7 +7,7 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
class SeedPhraseCheckUiStateBuilder( internal class SeedPhraseCheckUiStateBuilder(
private val state: MutableStateFlow<SeedPhraseState>, private val state: MutableStateFlow<SeedPhraseState>,
private val currentUiState: () -> MultiWalletSeedPhraseUM, private val currentUiState: () -> MultiWalletSeedPhraseUM,
private val updateUiState: ( private val updateUiState: (

View file

@ -10,7 +10,7 @@ import androidx.compose.ui.Modifier
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM
@Composable @Composable
fun MultiWalletSeedPhrase(state: MultiWalletSeedPhraseUM, modifier: Modifier = Modifier) { internal fun MultiWalletSeedPhrase(state: MultiWalletSeedPhraseUM, modifier: Modifier = Modifier) {
AnimatedContent( AnimatedContent(
modifier = modifier.navigationBarsPadding(), modifier = modifier.navigationBarsPadding(),
targetState = state, targetState = state,

View file

@ -6,13 +6,14 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.OutlinedTextField import androidx.compose.material.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -30,8 +31,26 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.st
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.utils.InvalidWordsColorTransformation import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.utils.InvalidWordsColorTransformation
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@Suppress("LongMethod")
@Composable @Composable
fun MultiWalletSeedPhraseImport(state: MultiWalletSeedPhraseUM.Import, modifier: Modifier = Modifier) { internal fun MultiWalletSeedPhraseImport(state: MultiWalletSeedPhraseUM.Import, modifier: Modifier = Modifier) {
if (state.dialog != null) {
BasicDialog(
title = state.dialog.title.resolveReference(),
message = state.dialog.message.resolveReference(),
confirmButton = DialogButtonUM(
title = state.dialog.confirmButtonText.resolveReference(),
onClick = state.dialog.onConfirmClick,
),
dismissButton = DialogButtonUM(
title = state.dialog.dismissButtonText.resolveReference(),
warning = state.dialog.dismissWarningColor,
onClick = state.dialog.onDismissButtonClick,
),
onDismissDialog = state.dialog.onDismiss,
)
}
Box(modifier.fillMaxSize()) { Box(modifier.fillMaxSize()) {
Column( Column(
modifier = Modifier modifier = Modifier
@ -109,14 +128,16 @@ private fun PhraseBlock(state: MultiWalletSeedPhraseUM.Import, modifier: Modifie
Column( Column(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
) { ) {
// TODO migrate to material3
OutlinedTextField( OutlinedTextField(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(TangemTheme.dimens.size142), .height(TangemTheme.dimens.size142),
value = state.passPhrase, value = state.words,
onValueChange = state.passPhraseChange, onValueChange = state.wordsChange,
textStyle = TangemTheme.typography.body1, textStyle = TangemTheme.typography.body1,
singleLine = false, singleLine = false,
colors = TangemTextFieldsDefault.defaultTextFieldColors,
visualTransformation = invalidWordsColorTransformation, visualTransformation = invalidWordsColorTransformation,
) )
@ -151,6 +172,7 @@ private fun SuggestionsBlock(
visible = suggestionsList.isNotEmpty(), visible = suggestionsList.isNotEmpty(),
) { ) {
LazyRow( LazyRow(
modifier = Modifier.padding(bottom = 8.dp),
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.size16), contentPadding = PaddingValues(horizontal = TangemTheme.dimens.size16),
) { ) {
items(suggestionsList.size) { index -> items(suggestionsList.size) { index ->
@ -171,13 +193,14 @@ private fun SuggestionsBlock(
@Composable @Composable
private fun SuggestionButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) { private fun SuggestionButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
Box(modifier = modifier.clickable(onClick = onClick)) { Notifier(
Notifier( text = text,
text = text, modifier = modifier
backgroundColor = TangemTheme.colors.icon.primary1, .clip(TangemTheme.shapes.roundedCorners8)
textColor = TangemTheme.colors.text.primary2, .clickable(onClick = onClick),
) 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) { private fun Modifier.rowPadding(index: Int, rowSize: Int, outSide: Dp, inSide: Dp): Modifier = when (index) {

View file

@ -26,7 +26,10 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@Composable @Composable
fun MultiWalletSeedPhraseWords(state: MultiWalletSeedPhraseUM.GenerateSeedPhrase, modifier: Modifier = Modifier) { internal fun MultiWalletSeedPhraseWords(
state: MultiWalletSeedPhraseUM.GenerateSeedPhrase,
modifier: Modifier = Modifier,
) {
Column(modifier.fillMaxSize()) { Column(modifier.fillMaxSize()) {
SegmentSeedBlock( SegmentSeedBlock(
modifier = Modifier.padding(vertical = 20.dp), modifier = Modifier.padding(vertical = 20.dp),

View file

@ -13,18 +13,38 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R import com.tangem.core.ui.R
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.components.OutlineTextField import com.tangem.core.ui.components.OutlineTextField
import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@Composable @Composable
fun MultiWalletSeedPhraseWordsCheck( internal fun MultiWalletSeedPhraseWordsCheck(
state: MultiWalletSeedPhraseUM.GeneratedWordsCheck, state: MultiWalletSeedPhraseUM.GeneratedWordsCheck,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
if (state.dialog != null) {
BasicDialog(
title = state.dialog.title.resolveReference(),
message = state.dialog.message.resolveReference(),
confirmButton = DialogButtonUM(
title = state.dialog.confirmButtonText.resolveReference(),
onClick = state.dialog.onConfirmClick,
),
dismissButton = DialogButtonUM(
title = state.dialog.dismissButtonText.resolveReference(),
warning = state.dialog.dismissWarningColor,
onClick = state.dialog.onDismissButtonClick,
),
onDismissDialog = state.dialog.onDismiss,
)
}
Column( Column(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()

View file

@ -4,11 +4,12 @@ import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@Immutable @Immutable
sealed class MultiWalletSeedPhraseUM( internal sealed class MultiWalletSeedPhraseUM(
val order: Int, val order: Int,
) { ) {
@ -35,6 +36,7 @@ sealed class MultiWalletSeedPhraseUM(
val createWalletButtonEnabled: Boolean = false, val createWalletButtonEnabled: Boolean = false,
val createWalletButtonProgress: Boolean = false, val createWalletButtonProgress: Boolean = false,
val onCreateWalletButtonClick: () -> Unit = {}, val onCreateWalletButtonClick: () -> Unit = {},
val dialog: OnboardingDialogUM? = null,
) : MultiWalletSeedPhraseUM(order = 2) { ) : MultiWalletSeedPhraseUM(order = 2) {
data class WordField( data class WordField(
val index: Int, val index: Int,
@ -58,5 +60,6 @@ sealed class MultiWalletSeedPhraseUM(
val suggestionsList: ImmutableList<String> = persistentListOf(), val suggestionsList: ImmutableList<String> = persistentListOf(),
val onSuggestionClick: (String) -> Unit = {}, val onSuggestionClick: (String) -> Unit = {},
val infoBottomSheetConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty, val infoBottomSheetConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty,
val dialog: OnboardingDialogUM? = null,
) : MultiWalletSeedPhraseUM(order = 1) ) : MultiWalletSeedPhraseUM(order = 1)
} }

View file

@ -0,0 +1,15 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.common.ui
import com.tangem.common.ui.alerts.models.AlertUM
import com.tangem.core.ui.extensions.TextReference
internal data class OnboardingDialogUM(
override val title: TextReference,
override val message: TextReference,
val dismissButtonText: TextReference,
override val confirmButtonText: TextReference,
val dismissWarningColor: Boolean = false,
override val onConfirmClick: () -> Unit,
val onDismissButtonClick: () -> Unit,
val onDismiss: () -> Unit,
) : AlertUM

View file

@ -1,18 +1,17 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.model package com.tangem.features.onboarding.v2.multiwallet.impl.common.ui
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state.MultiWalletCreateWalletUM
internal fun resetCardDialog(onConfirm: () -> Unit, dismiss: () -> Unit, onDismissButtonClick: () -> Unit) = internal fun resetCardDialog(onConfirm: () -> Unit, dismiss: () -> Unit, onDismissButtonClick: () -> Unit) =
MultiWalletCreateWalletUM.Dialog( OnboardingDialogUM(
title = resourceReference(R.string.onboarding_activation_error_title), title = resourceReference(R.string.onboarding_activation_error_title),
description = resourceReference(R.string.onboarding_activation_error_message), message = resourceReference(R.string.onboarding_activation_error_message),
confirmButtonText = resourceReference(R.string.common_support), confirmButtonText = resourceReference(R.string.common_support),
dismissButtonText = resourceReference(R.string.common_ok), dismissButtonText = resourceReference(R.string.common_ok),
dismissWarningColor = true, dismissWarningColor = true,
onDismiss = dismiss, onDismiss = dismiss,
onConfirm = { onConfirmClick = {
onConfirm() onConfirm()
dismiss() dismiss()
}, },