Updated on 2026-08-14
This commit is contained in:
parent
5a6dfd50e4
commit
c6497a0abc
19 changed files with 448 additions and 54 deletions
32
.idea/inspectionProfiles/Project_Default.xml
generated
32
.idea/inspectionProfiles/Project_Default.xml
generated
|
|
@ -4,8 +4,36 @@
|
|||
<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="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="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="KotlinRedundantOverride" 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="previewFile" value="true" />
|
||||
</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">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ dependencies {
|
|||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.navigation)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.feedback)
|
||||
|
|
@ -44,7 +47,7 @@ dependencies {
|
|||
implementation(deps.lifecycle.runtime.ktx)
|
||||
|
||||
/** 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.animation)
|
||||
implementation(deps.compose.foundation)
|
||||
|
|
|
|||
|
|
@ -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 kotlinx.coroutines.launch
|
||||
|
||||
class MultiWalletCreateWalletComponent(
|
||||
internal class MultiWalletCreateWalletComponent(
|
||||
context: AppComponentContext,
|
||||
params: MultiWalletChildParams,
|
||||
onNextStep: (OnboardingMultiWalletState.Step) -> Unit,
|
||||
|
|
|
|||
|
|
@ -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.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.common.ui.resetCardDialog
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -24,7 +25,7 @@ import kotlinx.coroutines.launch
|
|||
import javax.inject.Inject
|
||||
|
||||
@ComponentScoped
|
||||
class MultiWalletCreateWalletModel @Inject constructor(
|
||||
internal class MultiWalletCreateWalletModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val tangemSdkManager: TangemSdkManager,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
@Composable
|
||||
fun MultiWalletCreateWallet(state: MultiWalletCreateWalletUM, modifier: Modifier = Modifier) {
|
||||
internal fun MultiWalletCreateWallet(state: MultiWalletCreateWalletUM, modifier: Modifier = Modifier) {
|
||||
if (state.dialog != null) {
|
||||
BasicDialog(
|
||||
title = state.dialog.title.resolveReference(),
|
||||
message = state.dialog.description.resolveReference(),
|
||||
message = state.dialog.message.resolveReference(),
|
||||
confirmButton = DialogButtonUM(
|
||||
title = state.dialog.confirmButtonText.resolveReference(),
|
||||
onClick = state.dialog.onConfirm,
|
||||
onClick = state.dialog.onConfirmClick,
|
||||
),
|
||||
dismissButton = DialogButtonUM(
|
||||
title = state.dialog.dismissButtonText.resolveReference(),
|
||||
|
|
|
|||
|
|
@ -1,23 +1,13 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state
|
||||
|
||||
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 bodyText: TextReference,
|
||||
val showOtherOptionsButton: Boolean,
|
||||
val onCreateWalletClick: () -> Unit,
|
||||
val onOtherOptionsClick: () -> Unit,
|
||||
val dialog: Dialog?,
|
||||
) {
|
||||
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,
|
||||
val dialog: OnboardingDialogUM?,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,9 +1,15 @@
|
|||
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.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
|
|
@ -16,20 +22,24 @@ import kotlinx.coroutines.flow.SharedFlow
|
|||
import kotlinx.coroutines.launch
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
class MultiWalletSeedPhraseComponent(
|
||||
internal class MultiWalletSeedPhraseComponent(
|
||||
context: AppComponentContext,
|
||||
params: MultiWalletChildParams,
|
||||
onNextStep: (OnboardingMultiWalletState.Step) -> Unit,
|
||||
backButtonClickFlow: SharedFlow<Unit>,
|
||||
) : AppComponentContext by context, MultiWalletChildComponent {
|
||||
|
||||
private val model: MultiWalletSeedPhraseModel = getOrCreateModel()
|
||||
private val model: MultiWalletSeedPhraseModel = getOrCreateModel(params)
|
||||
|
||||
init {
|
||||
componentScope.launch {
|
||||
backButtonClickFlow.collect {
|
||||
model.onBack()
|
||||
backButtonClickFlow.collect { 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()
|
||||
}
|
||||
|
||||
// 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(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Context.findActivity(): Activity {
|
||||
var context = this
|
||||
while (context is ContextWrapper) {
|
||||
if (context is Activity) return context
|
||||
context = context.baseContext
|
||||
}
|
||||
error("")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +1,78 @@
|
|||
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.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
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.ImportSeedPhraseUiStateBuilder
|
||||
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.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 kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ComponentScoped
|
||||
class MultiWalletSeedPhraseModel @Inject constructor(
|
||||
internal class MultiWalletSeedPhraseModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val mnemonicRepository: MnemonicRepository,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val tangemSdkManager: TangemSdkManager,
|
||||
private val cardRepository: CardRepository,
|
||||
private val getCardInfoUseCase: GetCardInfoUseCase,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<MultiWalletChildParams>()
|
||||
private val multiWalletState get() = params.multiWalletState
|
||||
private val state = MutableStateFlow(SeedPhraseState())
|
||||
private val _uiState = MutableStateFlow(getStartState())
|
||||
|
||||
private val generateSeedPhraseUiStateBuilder = GenerateSeedPhraseUiStateBuilder(
|
||||
state = state,
|
||||
updateUiState = { block -> updateUiStateSpecific(block) },
|
||||
onContinue = { openWordsCheck() },
|
||||
onContinue = ::openWordsCheck,
|
||||
)
|
||||
|
||||
private val seedPhraseCheckUiStateBuilder = SeedPhraseCheckUiStateBuilder(
|
||||
state = state,
|
||||
currentUiState = { _uiState.value },
|
||||
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 onDone = MutableSharedFlow<Unit>()
|
||||
val navigateBack = MutableSharedFlow<Unit>()
|
||||
|
||||
fun onBack() {
|
||||
when (_uiState.value) {
|
||||
is MultiWalletSeedPhraseUM.Start -> {
|
||||
// TODO navigate back
|
||||
modelScope.launch { navigateBack.emit(Unit) }
|
||||
}
|
||||
is MultiWalletSeedPhraseUM.Import,
|
||||
is MultiWalletSeedPhraseUM.GenerateSeedPhrase,
|
||||
|
|
@ -56,7 +88,9 @@ class MultiWalletSeedPhraseModel @Inject constructor(
|
|||
|
||||
private fun getStartState(): MultiWalletSeedPhraseUM {
|
||||
return MultiWalletSeedPhraseUM.Start(
|
||||
onImportSeedPhraseClicked = {},
|
||||
onImportSeedPhraseClicked = {
|
||||
openImportSeedPhrase()
|
||||
},
|
||||
onGenerateSeedPhraseClicked = {
|
||||
generateSeedPhrase()
|
||||
openGeneratedSeedPhrase()
|
||||
|
|
@ -73,8 +107,6 @@ class MultiWalletSeedPhraseModel @Inject constructor(
|
|||
val words12 = state.value.generatedWords12 ?: return
|
||||
val words24 = state.value.generatedWords24 ?: return
|
||||
|
||||
// TODO disallow screenshots
|
||||
|
||||
_uiState.value = generateSeedPhraseUiStateBuilder.getState(
|
||||
generatedWords12 = words12,
|
||||
generatedWords24 = words24,
|
||||
|
|
@ -85,6 +117,10 @@ class MultiWalletSeedPhraseModel @Inject constructor(
|
|||
_uiState.value = seedPhraseCheckUiStateBuilder.getState()
|
||||
}
|
||||
|
||||
private fun openImportSeedPhrase() {
|
||||
_uiState.value = importSeedPhraseUiStateBuilder.getState()
|
||||
}
|
||||
|
||||
private fun generateSeedPhrase() {
|
||||
val words12 = mnemonicRepository.generateMnemonic(MnemonicRepository.MnemonicType.Words12)
|
||||
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) {
|
||||
_uiState.update {
|
||||
if (it !is T) return@update it
|
||||
|
|
|
|||
|
|
@ -6,5 +6,7 @@ data class SeedPhraseState(
|
|||
val generatedWords12: Mnemonic? = null,
|
||||
val generatedWords24: Mnemonic? = null,
|
||||
val words24Option: Boolean = false,
|
||||
val importedMnemonic: Mnemonic? = null,
|
||||
val passphrase: String? = null,
|
||||
val readyToImport: Boolean = false,
|
||||
)
|
||||
|
|
@ -8,7 +8,7 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class GenerateSeedPhraseUiStateBuilder(
|
||||
internal class GenerateSeedPhraseUiStateBuilder(
|
||||
private val state: MutableStateFlow<SeedPhraseState>,
|
||||
private val updateUiState: (
|
||||
(MultiWalletSeedPhraseUM.GenerateSeedPhrase) -> MultiWalletSeedPhraseUM.GenerateSeedPhrase,
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class SeedPhraseCheckUiStateBuilder(
|
||||
internal class SeedPhraseCheckUiStateBuilder(
|
||||
private val state: MutableStateFlow<SeedPhraseState>,
|
||||
private val currentUiState: () -> MultiWalletSeedPhraseUM,
|
||||
private val updateUiState: (
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import androidx.compose.ui.Modifier
|
|||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM
|
||||
|
||||
@Composable
|
||||
fun MultiWalletSeedPhrase(state: MultiWalletSeedPhraseUM, modifier: Modifier = Modifier) {
|
||||
internal fun MultiWalletSeedPhrase(state: MultiWalletSeedPhraseUM, modifier: Modifier = Modifier) {
|
||||
AnimatedContent(
|
||||
modifier = modifier.navigationBarsPadding(),
|
||||
targetState = state,
|
||||
|
|
|
|||
|
|
@ -6,13 +6,14 @@ import androidx.compose.foundation.layout.*
|
|||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material.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.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
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 kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@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()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
|
@ -109,14 +128,16 @@ private fun PhraseBlock(state: MultiWalletSeedPhraseUM.Import, modifier: Modifie
|
|||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
// TODO migrate to material3
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens.size142),
|
||||
value = state.passPhrase,
|
||||
onValueChange = state.passPhraseChange,
|
||||
value = state.words,
|
||||
onValueChange = state.wordsChange,
|
||||
textStyle = TangemTheme.typography.body1,
|
||||
singleLine = false,
|
||||
colors = TangemTextFieldsDefault.defaultTextFieldColors,
|
||||
visualTransformation = invalidWordsColorTransformation,
|
||||
)
|
||||
|
||||
|
|
@ -151,6 +172,7 @@ private fun SuggestionsBlock(
|
|||
visible = suggestionsList.isNotEmpty(),
|
||||
) {
|
||||
LazyRow(
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.size16),
|
||||
) {
|
||||
items(suggestionsList.size) { index ->
|
||||
|
|
@ -171,14 +193,15 @@ private fun SuggestionsBlock(
|
|||
|
||||
@Composable
|
||||
private fun SuggestionButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Box(modifier = modifier.clickable(onClick = onClick)) {
|
||||
Notifier(
|
||||
text = text,
|
||||
modifier = modifier
|
||||
.clip(TangemTheme.shapes.roundedCorners8)
|
||||
.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) {
|
||||
0 -> this.padding(start = outSide, end = inSide)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,10 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun MultiWalletSeedPhraseWords(state: MultiWalletSeedPhraseUM.GenerateSeedPhrase, modifier: Modifier = Modifier) {
|
||||
internal fun MultiWalletSeedPhraseWords(
|
||||
state: MultiWalletSeedPhraseUM.GenerateSeedPhrase,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier.fillMaxSize()) {
|
||||
SegmentSeedBlock(
|
||||
modifier = Modifier.padding(vertical = 20.dp),
|
||||
|
|
|
|||
|
|
@ -13,18 +13,38 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.PrimaryButtonIconEnd
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
fun MultiWalletSeedPhraseWordsCheck(
|
||||
internal fun MultiWalletSeedPhraseWordsCheck(
|
||||
state: MultiWalletSeedPhraseUM.GeneratedWordsCheck,
|
||||
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(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ import androidx.compose.runtime.Immutable
|
|||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
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.persistentListOf
|
||||
|
||||
@Immutable
|
||||
sealed class MultiWalletSeedPhraseUM(
|
||||
internal sealed class MultiWalletSeedPhraseUM(
|
||||
val order: Int,
|
||||
) {
|
||||
|
||||
|
|
@ -35,6 +36,7 @@ sealed class MultiWalletSeedPhraseUM(
|
|||
val createWalletButtonEnabled: Boolean = false,
|
||||
val createWalletButtonProgress: Boolean = false,
|
||||
val onCreateWalletButtonClick: () -> Unit = {},
|
||||
val dialog: OnboardingDialogUM? = null,
|
||||
) : MultiWalletSeedPhraseUM(order = 2) {
|
||||
data class WordField(
|
||||
val index: Int,
|
||||
|
|
@ -58,5 +60,6 @@ sealed class MultiWalletSeedPhraseUM(
|
|||
val suggestionsList: ImmutableList<String> = persistentListOf(),
|
||||
val onSuggestionClick: (String) -> Unit = {},
|
||||
val infoBottomSheetConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty,
|
||||
val dialog: OnboardingDialogUM? = null,
|
||||
) : MultiWalletSeedPhraseUM(order = 1)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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.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) =
|
||||
MultiWalletCreateWalletUM.Dialog(
|
||||
OnboardingDialogUM(
|
||||
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),
|
||||
dismissButtonText = resourceReference(R.string.common_ok),
|
||||
dismissWarningColor = true,
|
||||
onDismiss = dismiss,
|
||||
onConfirm = {
|
||||
onConfirmClick = {
|
||||
onConfirm()
|
||||
dismiss()
|
||||
},
|
||||
Loading…
Add table
Add a link
Reference in a new issue