Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-19 10:52:54 +03:00
parent 4b3d59fa0f
commit e6c8647f03
14 changed files with 1132 additions and 27 deletions

View file

@ -4,7 +4,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.FaultyDecomposeApi
import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.fade
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.slide
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.stackAnimation
import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState
@ -22,6 +24,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChild
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.MultiWalletBackupComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.chooseoption.Wallet1ChooseOptionComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.MultiWalletCreateWalletComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.MultiWalletSeedPhraseComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletModel
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState.Step.*
@ -30,6 +33,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.ui.WalletArtworksState
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor(
@ -44,7 +48,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
when (model.state.value.currentStep) {
CreateWallet -> WalletArtworksState.Folded
ChooseBackupOption -> WalletArtworksState.Fan
SeedPhrase -> WalletArtworksState.Hidden
SeedPhrase -> WalletArtworksState.Folded
AddBackupDevice -> WalletArtworksState.Unfold(WalletArtworksState.Unfold.Step.First)
FinishBackup -> WalletArtworksState.Stack()
Done -> error("Done state should not be used as starting state")
@ -95,7 +99,15 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
context = childContext,
onNextStep = ::handleNavigationEvent,
)
SeedPhrase -> TODO()
SeedPhrase -> MultiWalletSeedPhraseComponent(
context = childContext,
params = MultiWalletChildParams(
multiWalletState = model.state,
parentParams = params,
),
onNextStep = ::handleNavigationEvent,
backButtonClickFlow = MutableSharedFlow(),
)
AddBackupDevice -> MultiWalletBackupComponent(
context = childContext,
params = MultiWalletChildParams(
@ -115,7 +127,6 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
artworksState.value = WalletArtworksState.Fan
}
SeedPhrase -> {
artworksState.value = WalletArtworksState.Hidden
}
AddBackupDevice -> {
artworksState.value = WalletArtworksState.Unfold(WalletArtworksState.Unfold.Step.First)
@ -144,6 +155,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
}
}
@OptIn(FaultyDecomposeApi::class)
@Composable
override fun Content(modifier: Modifier) {
val stackState by childStack.subscribeAsState()
@ -154,10 +166,19 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
state = state,
modifier = modifier,
artworksState = artworksState,
isSeedPhraseState = stackState.active.configuration == SeedPhrase,
childContent = { mdfr ->
Children(
stack = stackState,
animation = stackAnimation(slide()),
animation = stackAnimation(
selector = { one, two, direction ->
if (one.configuration == SeedPhrase || two.configuration == SeedPhrase) {
fade()
} else {
slide()
}
},
),
) {
it.instance.Content(mdfr)
}

View file

@ -0,0 +1,44 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.MultiWalletSeedPhraseModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.MultiWalletSeedPhrase
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
@Suppress("UnusedPrivateMember")
class MultiWalletSeedPhraseComponent(
context: AppComponentContext,
params: MultiWalletChildParams,
onNextStep: (OnboardingMultiWalletState.Step) -> Unit,
backButtonClickFlow: SharedFlow<Unit>,
) : AppComponentContext by context, MultiWalletChildComponent {
private val model: MultiWalletSeedPhraseModel = getOrCreateModel()
init {
componentScope.launch {
backButtonClickFlow.collect {
model.onBack()
}
}
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
MultiWalletSeedPhrase(
modifier = modifier,
state = state,
)
}
}

View file

@ -0,0 +1,56 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
@ComponentScoped
class MultiWalletSeedPhraseModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val state = MutableStateFlow(SeedPhraseState())
private val _uiState = MutableStateFlow<MultiWalletSeedPhraseUM>(
MultiWalletSeedPhraseUM.Start(
onImportSeedPhraseClicked = {},
onGenerateSeedPhraseClicked = {
openGeneratedSeedPhrase()
},
onLearnMoreClicked = {},
),
)
val uiState = _uiState.asStateFlow()
fun onBack() {
TODO("Not yet implemented")
}
private fun openGeneratedSeedPhrase() {
// TODO state.value.generatedWords
val words = listOf("word, word")
_uiState.value = MultiWalletSeedPhraseUM.GenerateSeedPhrase(
words24OptionSelected = false,
words = words.mapIndexed { index, s ->
MultiWalletSeedPhraseUM.GenerateSeedPhrase.MnemonicGridItem(index, s)
}.toImmutableList(),
onWords24OptionSwitch = {
_uiState.update {
if (it !is MultiWalletSeedPhraseUM.GenerateSeedPhrase) return@update it
it.copy(words24OptionSelected = it.words24OptionSelected.not())
}
state.update { it.copy(words24Option = it.words24Option.not()) }
},
onContinueClick = {
},
)
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model
data class SeedPhraseState(
val generatedWords: List<String>? = null,
val words24Option: Boolean = false,
)

View file

@ -0,0 +1,37 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.core.tween
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.runtime.Composable
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) {
AnimatedContent(
modifier = modifier.navigationBarsPadding(),
targetState = state,
transitionSpec = {
val direction = if (initialState.order < targetState.order) {
AnimatedContentTransitionScope.SlideDirection.Start
} else {
AnimatedContentTransitionScope.SlideDirection.End
}
slideIntoContainer(towards = direction, animationSpec = tween())
.togetherWith(slideOutOfContainer(towards = direction, animationSpec = tween()))
},
contentKey = { st -> st::class.java },
label = "animatedContent",
) { st ->
when (st) {
is MultiWalletSeedPhraseUM.Start -> MultiWalletSeedPhraseStart(state = st)
is MultiWalletSeedPhraseUM.GenerateSeedPhrase -> MultiWalletSeedPhraseWords(state = st)
is MultiWalletSeedPhraseUM.GeneratedWordsCheck -> MultiWalletSeedPhraseWordsCheck(state = st)
is MultiWalletSeedPhraseUM.Import -> MultiWalletSeedPhraseImport(state = st)
}
}
}

View file

@ -0,0 +1,201 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui
import androidx.compose.animation.*
import androidx.compose.foundation.clickable
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.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.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.*
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.utils.InvalidWordsColorTransformation
import kotlinx.collections.immutable.ImmutableList
@Composable
fun MultiWalletSeedPhraseImport(state: MultiWalletSeedPhraseUM.Import, modifier: Modifier = Modifier) {
Box(modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
Text(
text = stringResource(id = R.string.onboarding_seed_import_message),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(start = 36.dp, end = 36.dp, top = 24.dp, bottom = 16.dp)
.fillMaxWidth(),
)
PhraseBlock(
state = state,
modifier = Modifier.padding(horizontal = 16.dp),
)
OutlineTextFieldWithIcon(
modifier = modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
value = state.passPhrase,
onValueChange = state.passPhraseChange,
iconResId = R.drawable.ic_information_24,
iconColor = TangemTheme.colors.icon.informative,
label = stringResource(id = R.string.common_passphrase),
placeholder = stringResource(id = R.string.send_optional_field),
onIconClick = state.onPassphraseInfoClick,
)
Box(Modifier.weight(1f)) {
PrimaryButtonIconEnd(
modifier = Modifier
.align(Alignment.BottomStart)
.padding(16.dp)
.fillMaxWidth(),
text = stringResource(id = R.string.common_import),
iconResId = R.drawable.ic_tangem_24,
enabled = state.createWalletEnabled,
showProgress = state.createWalletProgress,
onClick = state.createWalletClick,
)
}
}
val keyboard by keyboardAsState()
if (keyboard is Keyboard.Opened) {
SuggestionsBlock(
modifier = Modifier
.align(Alignment.BottomCenter)
.imePadding(),
suggestionsList = state.suggestionsList,
onClick = { index -> state.onSuggestionClick(state.suggestionsList[index]) },
)
}
}
PassphraseInfoBottomSheet(state.infoBottomSheetConfig)
}
@Composable
private fun PhraseBlock(state: MultiWalletSeedPhraseUM.Import, modifier: Modifier = Modifier) {
val warningColor = TangemTheme.colors.text.warning
val invalidWordsColorTransformation = remember(state.invalidWords, warningColor) {
InvalidWordsColorTransformation(
wordsToBrush = state.invalidWords,
style = SpanStyle(color = warningColor),
)
}
Column(
modifier = modifier.fillMaxWidth(),
) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size142),
value = state.passPhrase,
onValueChange = state.passPhraseChange,
textStyle = TangemTheme.typography.body1,
singleLine = false,
visualTransformation = invalidWordsColorTransformation,
)
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.height(TangemTheme.dimens.size32),
) {
if (state.wordsErrorText != null) {
Text(
modifier = Modifier.fillMaxSize(),
text = state.wordsErrorText.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.warning,
)
}
}
}
}
@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(showBackground = true)
@Composable
private fun Preview() {
TangemThemePreview(
alwaysShowBottomSheets = false,
) {
MultiWalletSeedPhraseImport(
state = MultiWalletSeedPhraseUM.Import(
wordsErrorText = stringReference("Error"),
),
)
}
}

View file

@ -0,0 +1,158 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui
import androidx.compose.foundation.*
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.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.LineBreak
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.components.SecondaryButton
import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.components.SpacerW8
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM
@Composable
internal fun MultiWalletSeedPhraseStart(state: MultiWalletSeedPhraseUM.Start, modifier: Modifier = Modifier) {
Column(
modifier.padding(top = 48.dp),
) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.weight(1f),
verticalArrangement = Arrangement.spacedBy(28.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
modifier = Modifier.size(56.dp),
imageVector = ImageVector.vectorResource(R.drawable.ic_onboarding_text_edit_56),
contentDescription = null,
)
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 = stringResource(id = R.string.onboarding_seed_phrase_intro_legacy),
color = TangemTheme.colors.text.warning,
style = TangemTheme.typography.body1,
)
}
BodyContent(state)
}
SpacerH32()
Buttons(state)
}
}
@Composable
private fun BodyContent(state: MultiWalletSeedPhraseUM.Start, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text(
text = stringResource(id = R.string.onboarding_seed_intro_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = stringResource(id = R.string.onboarding_seed_intro_message),
style = TangemTheme.typography.body1
.copy(lineBreak = LineBreak.Paragraph),
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(horizontal = 32.dp)
.fillMaxWidth(),
)
ReadMoreBlock(state)
}
}
@Composable
private fun ReadMoreBlock(state: MultiWalletSeedPhraseUM.Start, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxWidth()
.padding(top = 16.dp),
) {
val shape = TangemTheme.shapes.roundedCornersLarge
Row(
modifier = Modifier
.align(Alignment.Center)
.clip(shape)
.border(
width = 1.dp,
color = TangemTheme.colors.stroke.primary,
shape = shape,
)
.clickable(onClick = state.onLearnMoreClicked)
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.Center,
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_top_right_24),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
)
SpacerW8()
Text(
modifier = Modifier.align(Alignment.CenterVertically),
style = TangemTheme.typography.button,
text = stringResource(id = R.string.onboarding_seed_button_read_more),
)
}
}
}
@Composable
private fun Buttons(state: MultiWalletSeedPhraseUM.Start, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.imePadding(),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
SecondaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.onboarding_seed_intro_button_generate),
onClick = state.onGenerateSeedPhraseClicked,
)
SecondaryButton(
modifier = Modifier
.fillMaxWidth(),
text = stringResource(id = R.string.onboarding_seed_intro_button_import),
onClick = state.onImportSeedPhraseClicked,
)
}
}
@Preview(showBackground = true)
@Composable
private fun Preview() {
TangemThemePreview {
MultiWalletSeedPhraseStart(
state = MultiWalletSeedPhraseUM.Start(),
)
}
}

View file

@ -0,0 +1,208 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui
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.platform.LocalLayoutDirection
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.impl.R
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.GenerateSeedPhrase.MnemonicGridItem
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Composable
fun MultiWalletSeedPhraseWords(state: MultiWalletSeedPhraseUM.GenerateSeedPhrase, modifier: Modifier = Modifier) {
Column(modifier.fillMaxSize()) {
SegmentSeedBlock(
modifier = Modifier.padding(vertical = 20.dp),
state = state,
)
TitleBlock(state)
SeedPhraseGridBlock(
mnemonicGridItems = state.words,
modifier = Modifier
.padding(top = 20.dp, bottom = 92.dp),
)
Box(Modifier.weight(1f)) {
PrimaryButton(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
text = stringResource(id = R.string.common_continue),
onClick = state.onContinueClick,
)
}
}
}
@Suppress("ComplexCondition", "MagicNumber")
@Composable
private fun SegmentSeedBlock(state: MultiWalletSeedPhraseUM.GenerateSeedPhrase, modifier: Modifier = Modifier) {
SegmentedButtons(
modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing76),
config = persistentListOf(0, 1),
initialSelectedItem = if (state.words24OptionSelected.not()) 0 else 1,
onClick = {
if (state.words24OptionSelected && it == 1 || state.words24OptionSelected.not() && it == 0) {
state.onWords24OptionSwitch()
}
},
) {
Text(
text = pluralStringResource(
id = R.plurals.onboarding_seed_generate_words_count,
count = if (it == 0) 12 else 24,
if (it == 0) 12 else 24,
),
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing10)
.fillMaxWidth(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
}
}
@Suppress("MagicNumber")
@Composable
private fun TitleBlock(state: MultiWalletSeedPhraseUM.GenerateSeedPhrase, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.padding(horizontal = TangemTheme.dimens.size36)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text(
text = stringResource(id = R.string.onboarding_seed_generate_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
Text(
text = pluralStringResource(
id = R.plurals.onboarding_seed_generate_message_words_count,
count = if (state.words24OptionSelected.not()) 12 else 24,
if (state.words24OptionSelected.not()) 12 else 24,
),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
}
}
@Composable
private fun SeedPhraseGridBlock(mnemonicGridItems: ImmutableList<MnemonicGridItem>, modifier: Modifier = Modifier) {
VerticalGrid(
modifier = modifier
.verticalScroll(rememberScrollState()),
items = mnemonicGridItems,
) { item ->
Row(
modifier = Modifier.padding(all = TangemTheme.dimens.size8),
verticalAlignment = Alignment.CenterVertically,
) {
if (LocalLayoutDirection.current == LayoutDirection.Ltr) {
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,
)
} else {
Text(
text = item.mnemonic,
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary1,
)
Text(
modifier = Modifier.width(TangemTheme.dimens.size40),
text = "${item.index}.",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
}
}
}
}
@Composable
private inline fun <T> VerticalGrid(
items: ImmutableList<T>,
modifier: Modifier = Modifier,
crossinline content: @Composable (T) -> Unit,
) {
val columnLength = items.size / 2
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
for (index in 0 until 2) {
Column {
for (i in 0 until columnLength) {
val item = items[index * columnLength + i]
content(item)
}
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun Preview() {
TangemThemePreview {
MultiWalletSeedPhraseWords(
state = MultiWalletSeedPhraseUM.GenerateSeedPhrase(
words = persistentListOf(
MnemonicGridItem(
index = 0,
mnemonic = "word1",
),
MnemonicGridItem(
index = 1,
mnemonic = "word1",
),
MnemonicGridItem(
index = 2,
mnemonic = "word1",
),
MnemonicGridItem(
index = 3,
mnemonic = "word1",
),
MnemonicGridItem(
index = 4,
mnemonic = "word1",
),
),
),
)
}
}

View file

@ -0,0 +1,122 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui
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.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
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.OutlineTextField
import com.tangem.core.ui.components.PrimaryButtonIconEnd
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(
state: MultiWalletSeedPhraseUM.GeneratedWordsCheck,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
Text(
text = stringResource(R.string.onboarding_seed_user_validation_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 48.dp, start = 36.dp, end = 36.dp, bottom = 16.dp)
.fillMaxWidth(),
)
Text(
text = stringResource(R.string.onboarding_seed_user_validation_message),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(horizontal = 36.dp)
.fillMaxWidth(),
)
Fields(
state = state,
modifier = Modifier
.padding(vertical = 30.dp, horizontal = 16.dp),
)
Box(
Modifier.weight(1f),
) {
PrimaryButtonIconEnd(
modifier = Modifier
.align(Alignment.BottomStart)
.padding(16.dp)
.fillMaxWidth(),
text = stringResource(id = R.string.onboarding_create_wallet_button_create_wallet),
iconResId = R.drawable.ic_tangem_24,
enabled = state.createWalletButtonEnabled,
showProgress = state.createWalletButtonProgress,
onClick = state.onCreateWalletButtonClick,
)
}
}
}
@Composable
private fun Fields(state: MultiWalletSeedPhraseUM.GeneratedWordsCheck, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
state.wordFields.forEach { field ->
OutlineTextField(
value = field.word,
onValueChange = field.onChange,
label = field.index.toString(),
isError = field.error,
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun Preview() {
TangemThemePreview {
MultiWalletSeedPhraseWordsCheck(
state = MultiWalletSeedPhraseUM.GeneratedWordsCheck(
wordFields = persistentListOf(
MultiWalletSeedPhraseUM.GeneratedWordsCheck.WordField(
index = 2,
word = TextFieldValue(text = "word"),
onChange = {},
error = false,
),
MultiWalletSeedPhraseUM.GeneratedWordsCheck.WordField(
index = 7,
word = TextFieldValue(text = "wor"),
onChange = {},
error = true,
),
MultiWalletSeedPhraseUM.GeneratedWordsCheck.WordField(
index = 11,
word = TextFieldValue(text = "word"),
onChange = {},
error = false,
),
),
),
)
}
}

View file

@ -0,0 +1,90 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.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.res.stringResource
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.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.impl.R
@Composable
fun PassphraseInfoBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet(
config = config,
containerColor = TangemTheme.colors.background.primary,
) { _: TangemBottomSheetConfigContent.Empty ->
PassphraseInfoBottomSheetContent(config.onDismissRequest)
}
}
@Composable
fun PassphraseInfoBottomSheetContent(onDismiss: () -> Unit) {
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 = stringResource(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 = stringResource(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 = stringResource(id = R.string.common_ok),
onClick = onDismiss,
)
}
}
@Preview
@Composable
private fun PassphraseInfoBottomSheetContentPreview() {
TangemThemePreview {
PassphraseInfoBottomSheetContent({ })
}
}

View file

@ -0,0 +1,62 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state
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 kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Immutable
sealed class MultiWalletSeedPhraseUM(
val order: Int,
) {
data class Start(
val onLearnMoreClicked: () -> Unit = {},
val onGenerateSeedPhraseClicked: () -> Unit = {},
val onImportSeedPhraseClicked: () -> Unit = {},
) : MultiWalletSeedPhraseUM(order = 0)
data class GenerateSeedPhrase(
val words24OptionSelected: Boolean = false,
val words: ImmutableList<MnemonicGridItem> = persistentListOf(),
val onWords24OptionSwitch: () -> Unit = {},
val onContinueClick: () -> Unit = {},
) : MultiWalletSeedPhraseUM(order = 1) {
data class MnemonicGridItem(
val index: Int,
val mnemonic: String,
)
}
data class GeneratedWordsCheck(
val wordFields: ImmutableList<WordField> = persistentListOf(),
val createWalletButtonEnabled: Boolean = false,
val createWalletButtonProgress: Boolean = false,
val onCreateWalletButtonClick: () -> Unit = {},
) : MultiWalletSeedPhraseUM(order = 2) {
data class WordField(
val index: Int,
val word: TextFieldValue,
val error: Boolean,
val onChange: (TextFieldValue) -> Unit,
)
}
data class Import(
val words: TextFieldValue = TextFieldValue(""),
val wordsChange: (TextFieldValue) -> Unit = {},
val passPhrase: TextFieldValue = TextFieldValue(""),
val passPhraseChange: (TextFieldValue) -> Unit = {},
val onPassphraseInfoClick: () -> Unit = {},
val wordsErrorText: TextReference? = null,
val invalidWords: ImmutableList<String> = persistentListOf(),
val createWalletEnabled: Boolean = false,
val createWalletProgress: Boolean = false,
val createWalletClick: () -> Unit = {},
val suggestionsList: ImmutableList<String> = persistentListOf(),
val onSuggestionClick: (String) -> Unit = {},
val infoBottomSheetConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty,
) : MultiWalletSeedPhraseUM(order = 1)
}

View file

@ -0,0 +1,77 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.utils
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
import kotlinx.collections.immutable.ImmutableList
class InvalidWordsColorTransformation(
private val wordsToBrush: ImmutableList<String>,
private val style: SpanStyle,
) : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
val plainText = text.text
if (wordsToBrush.isSingleItem() && wordsToBrush.first() == plainText) {
return plainText.annotate().toTransformedText()
}
val wordList = makeSeparatedWordlist(plainText)
val annotatedString = makeAnnotatedString(wordList)
return annotatedString.toTransformedText()
}
private fun makeSeparatedWordlist(plainText: String): List<String> {
val plainTextCharArray = plainText.toCharArray()
val wordlist = mutableListOf<String>()
var wordBuilder = StringBuilder()
for (index in plainTextCharArray.indices) {
val char = plainTextCharArray[index]
if (char.isWhitespace()) {
if (wordBuilder.isEmpty()) {
wordlist.add(WORD_SEPARATOR)
} else {
wordlist.add(wordBuilder.toString())
wordBuilder = StringBuilder()
wordlist.add(WORD_SEPARATOR)
}
} else {
if (index == plainTextCharArray.lastIndex) {
wordBuilder.append(char)
wordlist.add(wordBuilder.toString())
wordBuilder = StringBuilder()
} else {
wordBuilder.append(char)
}
}
}
return wordlist.toList()
}
private fun makeAnnotatedString(wordList: List<String>): AnnotatedString = buildAnnotatedString {
wordList.forEach {
if (it == WORD_SEPARATOR) {
append(WHITE_SPACE)
} else if (wordsToBrush.contains(it)) {
append(it.annotate())
} else {
append(it)
}
}
}
private fun String.annotate(): AnnotatedString = AnnotatedString(this, style)
private fun AnnotatedString.toTransformedText(): TransformedText = TransformedText(this, OffsetMapping.Identity)
companion object {
private const val WHITE_SPACE = " "
private const val WORD_SEPARATOR = "_$@-*"
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletCo
import com.tangem.features.onboarding.v2.multiwallet.impl.DefaultOnboardingMultiWalletComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model.MultiWalletBackupModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.model.MultiWalletCreateWalletModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.MultiWalletSeedPhraseModel
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletModel
import dagger.Binds
import dagger.Module
@ -42,4 +43,9 @@ internal interface ModelModule {
@IntoMap
@ClassKey(MultiWalletCreateWalletModel::class)
fun provideModel3(model: MultiWalletCreateWalletModel): Model
@Binds
@IntoMap
@ClassKey(MultiWalletSeedPhraseModel::class)
fun provideModel4(model: MultiWalletSeedPhraseModel): Model
}

View file

@ -2,8 +2,6 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -18,33 +16,32 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.ui.state.OnboardingMul
internal fun OnboardingMultiWallet(
state: OnboardingMultiWalletUM,
artworksState: WalletArtworksState,
isSeedPhraseState: Boolean,
modifier: Modifier = Modifier,
childContent: @Composable (Modifier) -> Unit = {},
) {
Column(
modifier = modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
modifier = modifier.fillMaxSize(),
) {
WalletArtworks(
url = state.artworkUrl,
modifier = Modifier
.weight(.56f)
.padding(horizontal = 34.dp)
.padding(top = 20.dp)
.fillMaxWidth(),
state = artworksState,
)
if (isSeedPhraseState.not()) {
WalletArtworks(
url = state.artworkUrl,
modifier = Modifier
.weight(.56f)
.padding(horizontal = 34.dp)
.padding(top = 20.dp)
.fillMaxWidth(),
state = artworksState,
)
Box(
modifier = if (artworksState == WalletArtworksState.Hidden) {
Modifier.fillMaxSize()
} else {
Modifier.weight(.44f)
},
contentAlignment = Alignment.BottomStart,
) {
childContent(Modifier)
Box(
modifier = Modifier.weight(.44f),
contentAlignment = Alignment.BottomStart,
) {
childContent(Modifier)
}
} else {
childContent(Modifier.fillMaxSize())
}
}
}
@ -56,6 +53,7 @@ private fun Preview() {
OnboardingMultiWallet(
state = OnboardingMultiWalletUM(artworkUrl = null),
artworksState = WalletArtworksState.Folded,
isSeedPhraseState = false,
childContent = { md ->
Box(
modifier = md
@ -66,4 +64,23 @@ private fun Preview() {
},
)
}
}
@Preview(showBackground = true, widthDp = 360, heightDp = 640)
@Composable
private fun Preview2() {
TangemThemePreview {
OnboardingMultiWallet(
state = OnboardingMultiWalletUM(artworkUrl = null),
artworksState = WalletArtworksState.Folded,
isSeedPhraseState = true,
childContent = { md ->
Box(
modifier = md
.background(Color.DarkGray)
.fillMaxSize(),
)
},
)
}
}