Updated on 2026-08-14

This commit is contained in:
Tangem 2024-03-28 19:56:01 +00:00
parent 4df89bb521
commit cf5a7d68ad
16 changed files with 406 additions and 66 deletions

View file

@ -407,7 +407,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) { private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) {
val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0 val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0
val isNotScannedBefore = store.state.globalState.scanResponse == null val isNotScannedBefore = store.state.globalState.scanResponse == null
val isOnboardingServiceNotActive = store.state.globalState.onboardingState.onboardingStarted val isOnboardingServiceNotActive = !store.state.globalState.onboardingState.onboardingStarted
when { when {
!backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive -> { !backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive -> {

View file

@ -106,6 +106,7 @@ class TangemSdkManager(
suspend fun importWallet( suspend fun importWallet(
scanResponse: ScanResponse, scanResponse: ScanResponse,
mnemonic: String, mnemonic: String,
passphrase: String?,
shouldReset: Boolean, shouldReset: Boolean,
): CompletionResult<CreateProductWalletTaskResponse> { ): CompletionResult<CreateProductWalletTaskResponse> {
val defaultMnemonic = try { val defaultMnemonic = try {
@ -118,6 +119,7 @@ class TangemSdkManager(
cardTypesResolver = scanResponse.cardTypesResolver, cardTypesResolver = scanResponse.cardTypesResolver,
derivationStyleProvider = scanResponse.derivationStyleProvider, derivationStyleProvider = scanResponse.derivationStyleProvider,
mnemonic = defaultMnemonic, mnemonic = defaultMnemonic,
passphrase = passphrase,
shouldReset = shouldReset, shouldReset = shouldReset,
), ),
scanResponse.card.cardId, scanResponse.card.cardId,

View file

@ -62,6 +62,7 @@ class CreateProductWalletTask(
private val cardTypesResolver: CardTypesResolver, private val cardTypesResolver: CardTypesResolver,
private val derivationStyleProvider: DerivationStyleProvider, private val derivationStyleProvider: DerivationStyleProvider,
private val mnemonic: Mnemonic? = null, private val mnemonic: Mnemonic? = null,
private val passphrase: String? = null,
private val shouldReset: Boolean, private val shouldReset: Boolean,
) : CardSessionRunnable<CreateProductWalletTaskResponse> { ) : CardSessionRunnable<CreateProductWalletTaskResponse> {
@ -82,7 +83,7 @@ class CreateProductWalletTask(
cardTypesResolver.isTangemTwins() -> cardTypesResolver.isTangemTwins() ->
throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet") throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet")
else -> CreateWalletTangemWallet(mnemonic, shouldReset, derivationStyleProvider, cardDto) else -> CreateWalletTangemWallet(mnemonic, passphrase, shouldReset, derivationStyleProvider, cardDto)
} }
commandProcessor.proceed(cardDto, session) { commandProcessor.proceed(cardDto, session) {
when (it) { when (it) {
@ -142,6 +143,7 @@ private class CreateWalletTangemNote(private val cardTypesResolver: CardTypesRes
*/ */
private class CreateWalletTangemWallet( private class CreateWalletTangemWallet(
private val mnemonic: Mnemonic?, private val mnemonic: Mnemonic?,
private val passphrase: String?,
private val shouldReset: Boolean, private val shouldReset: Boolean,
private val derivationStyleProvider: DerivationStyleProvider, private val derivationStyleProvider: DerivationStyleProvider,
cardDTO: CardDTO, cardDTO: CardDTO,
@ -170,7 +172,7 @@ private class CreateWalletTangemWallet(
session: CardSession, session: CardSession,
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit, callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
) { ) {
CreateWalletsTask(cardConfig.mandatoryCurves, mnemonic).run(session) { result -> CreateWalletsTask(cardConfig.mandatoryCurves, mnemonic, passphrase).run(session) { result ->
when (result) { when (result) {
is CompletionResult.Success -> { is CompletionResult.Success -> {
checkIfAllWalletsCreated(card, session, result.data, callback) checkIfAllWalletsCreated(card, session, result.data, callback)

View file

@ -21,6 +21,7 @@ class CreateWalletsResponse(
class CreateWalletsTask( class CreateWalletsTask(
private val curves: List<EllipticCurve>, private val curves: List<EllipticCurve>,
private val mnemonic: Mnemonic? = null, private val mnemonic: Mnemonic? = null,
private val passphrase: String?,
) : CardSessionRunnable<CreateWalletsResponse> { ) : CardSessionRunnable<CreateWalletsResponse> {
private val createdWalletsResponses = mutableListOf<CreateWalletResponse>() private val createdWalletsResponses = mutableListOf<CreateWalletResponse>()
@ -41,7 +42,7 @@ class CreateWalletsTask(
callback: (result: CompletionResult<CreateWalletsResponse>) -> Unit, callback: (result: CompletionResult<CreateWalletsResponse>) -> Unit,
) { ) {
val extendedPrivateKey = mnemonic?.let { val extendedPrivateKey = mnemonic?.let {
AnyMasterKeyFactory(mnemonic = it, passphrase = "").makeMasterKey(curve) AnyMasterKeyFactory(mnemonic = it, passphrase = passphrase ?: "").makeMasterKey(curve)
} }
CreateWalletTask(curve, extendedPrivateKey).run(session) { result -> CreateWalletTask(curve, extendedPrivateKey).run(session) { result ->
when (result) { when (result) {

View file

@ -38,6 +38,7 @@ sealed class OnboardingWallet2Action : OnboardingWalletAction() {
data class ImportWallet( data class ImportWallet(
val mnemonicComponents: List<String>, val mnemonicComponents: List<String>,
val passphrase: String?,
val seedPhraseSource: SeedPhraseSource, val seedPhraseSource: SeedPhraseSource,
val callback: (CompletionResult<CreateWalletResponse>) -> Unit, val callback: (CompletionResult<CreateWalletResponse>) -> Unit,
) : OnboardingWallet2Action() ) : OnboardingWallet2Action()

View file

@ -271,6 +271,7 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) {
val mediateResult = when ( val mediateResult = when (
val result = tangemSdkManager.importWallet( val result = tangemSdkManager.importWallet(
scanResponse = scanResponse, scanResponse = scanResponse,
passphrase = action.passphrase,
mnemonic = action.mnemonicComponents.joinToString(" "), mnemonic = action.mnemonicComponents.joinToString(" "),
shouldReset = globalState.onboardingState.shouldResetOnCreate, shouldReset = globalState.onboardingState.shouldResetOnCreate,
) )

View file

@ -523,10 +523,18 @@ class OnboardingWalletFragment :
override fun importWallet( override fun importWallet(
mnemonicComponents: List<String>, mnemonicComponents: List<String>,
passphrase: String?,
seedPhraseSource: SeedPhraseSource, seedPhraseSource: SeedPhraseSource,
callback: (CompletionResult<CreateWalletResponse>) -> Unit, callback: (CompletionResult<CreateWalletResponse>) -> Unit,
) { ) {
store.dispatch(OnboardingWallet2Action.ImportWallet(mnemonicComponents, seedPhraseSource, callback)) store.dispatch(
OnboardingWallet2Action.ImportWallet(
mnemonicComponents,
passphrase,
seedPhraseSource,
callback,
),
)
} }
override fun allowScreenshots(allow: Boolean) { override fun allowScreenshots(allow: Boolean) {

View file

@ -1,5 +1,6 @@
package com.tangem.core.ui.components package com.tangem.core.ui.components
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
@ -59,6 +60,40 @@ fun OutlineTextField(
) )
} }
@Composable
fun OutlineTextFieldWithIcon(
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
@DrawableRes iconResId: Int,
iconColor: Color,
onIconClick: () -> Unit,
modifier: Modifier = Modifier,
label: String? = null,
placeholder: String? = null,
enabled: Boolean = true,
isError: Boolean = false,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
TangemTextFieldWithIcon(
modifier = modifier,
value = value,
onValueChange = onValueChange,
iconResId = iconResId,
iconColor = iconColor,
singleLine = true,
label = label,
placeholder = placeholder,
enabled = enabled,
isError = isError,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
interactionSource = interactionSource,
onIconClick = onIconClick,
)
}
// region Defaults // region Defaults
@Suppress("LongMethod") @Suppress("LongMethod")
@Composable @Composable
@ -168,6 +203,84 @@ private fun TangemTextField(
} }
} }
@Suppress("LongMethod")
@Composable
private fun TangemTextFieldWithIcon(
value: TextFieldValue,
singleLine: Boolean,
onValueChange: (TextFieldValue) -> Unit,
@DrawableRes iconResId: Int,
iconColor: Color,
modifier: Modifier = Modifier,
label: String? = null,
placeholder: String? = null,
enabled: Boolean = true,
isError: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
colors: TangemTextFieldColors = TangemTextFieldsDefault.defaultTextFieldColors,
size: TangemTextFieldSize = TangemTextFieldSize.Default,
onIconClick: () -> Unit = {},
) {
Column(modifier = modifier) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
.heightIn(size.toHeightDp()),
value = value,
textStyle = TangemTheme.typography.body1,
onValueChange = onValueChange,
singleLine = singleLine,
enabled = enabled,
isError = isError,
visualTransformation = visualTransformation,
keyboardActions = keyboardActions,
keyboardOptions = keyboardOptions,
interactionSource = interactionSource,
shape = size.toShape(),
colors = colors,
label = {
if (!label.isNullOrEmpty()) {
Text(
text = label,
style = TangemTheme.typography.caption2,
color = colors.labelColor(
enabled = enabled,
error = isError,
interactionSource = interactionSource,
).value,
)
}
},
placeholder = {
if (!placeholder.isNullOrEmpty()) {
Text(
text = placeholder,
style = TangemTheme.typography.body1,
color = colors.placeholderColor(enabled = enabled).value,
)
}
},
trailingIcon = {
IconButton(
modifier = Modifier.size(32.dp),
onClick = onIconClick,
) {
Icon(
modifier = Modifier.size(24.dp),
painter = painterResource(id = iconResId),
tint = iconColor,
contentDescription = "Clear input",
)
}
},
)
}
}
private enum class TangemTextFieldSize { private enum class TangemTextFieldSize {
Default, Default,
} }
@ -378,6 +491,16 @@ private fun OutlineTextFieldSample(modifier: Modifier = Modifier) {
caption = "Supporting text", caption = "Supporting text",
) )
Divider(modifier = Modifier.padding(vertical = 8.dp)) Divider(modifier = Modifier.padding(vertical = 8.dp))
OutlineTextFieldWithIcon(
value = TextFieldValue(),
onValueChange = { /* no-op */ },
iconResId = R.drawable.ic_alert_24,
iconColor = TangemTheme.colors.icon.informative,
label = "Default without value",
placeholder = "Placeholder",
onIconClick = {},
)
Divider(modifier = Modifier.padding(vertical = 8.dp))
} }
} }

View file

@ -1,6 +1,8 @@
package com.tangem.feature.onboarding.presentation.wallet2.model package com.tangem.feature.onboarding.presentation.wallet2.model
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.TangemBottomSheetConfigContent
import com.tangem.feature.onboarding.domain.SeedPhraseError import com.tangem.feature.onboarding.domain.SeedPhraseError
import com.tangem.feature.onboarding.domain.models.MnemonicType import com.tangem.feature.onboarding.domain.models.MnemonicType
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -64,11 +66,6 @@ enum class SegmentSeedType(val count: Int) {
} }
} }
data class MnemonicState(
val seedType: SegmentSeedType,
val mnemonicItems: PersistentList<MnemonicGridItem>,
)
data class MnemonicGridItem( data class MnemonicGridItem(
val index: Int, val index: Int,
val mnemonic: String, val mnemonic: String,
@ -82,14 +79,19 @@ data class CheckSeedPhraseState(
) )
data class ImportSeedPhraseState( data class ImportSeedPhraseState(
val tvSeedPhrase: TextFieldState, val fieldSeedPhrase: TextFieldState,
val fieldPassphrase: TextFieldState,
val onSuggestedPhraseClick: (Int) -> Unit, val onSuggestedPhraseClick: (Int) -> Unit,
val onPassphraseInfoClick: () -> Unit,
val buttonCreateWallet: ButtonState, val buttonCreateWallet: ButtonState,
val invalidWords: Set<String> = emptySet(), val invalidWords: Set<String> = emptySet(),
val suggestionsList: ImmutableList<String> = persistentListOf(), val suggestionsList: ImmutableList<String> = persistentListOf(),
val error: SeedPhraseError? = null, val error: SeedPhraseError? = null,
val bottomSheetConfig: TangemBottomSheetConfig? = null,
) )
data class ShowPassphraseInfoBottomSheetContent(val onOkClick: () -> Unit) : TangemBottomSheetConfigContent
data class TextFieldState( data class TextFieldState(
val onTextFieldValueChanged: (TextFieldValue) -> Unit, val onTextFieldValueChanged: (TextFieldValue) -> Unit,
val textFieldValue: TextFieldValue = TextFieldValue(), val textFieldValue: TextFieldValue = TextFieldValue(),

View file

@ -40,7 +40,9 @@ data class CheckSeedPhraseUiAction(
data class ImportSeedPhraseUiAction( data class ImportSeedPhraseUiAction(
val phraseTextFieldAction: TextFieldUiAction, val phraseTextFieldAction: TextFieldUiAction,
val passTextFieldAction: TextFieldUiAction,
val suggestedPhraseClick: (Int) -> Unit, val suggestedPhraseClick: (Int) -> Unit,
val onPassphraseInfoClick: () -> Unit,
val buttonCreateWalletClick: () -> Unit, val buttonCreateWalletClick: () -> Unit,
) )

View file

@ -1,13 +1,16 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui package com.tangem.feature.onboarding.presentation.wallet2.ui
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.material.OutlinedTextField import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -17,9 +20,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import com.tangem.core.ui.components.Notifier import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.TangemTextFieldsDefault
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.onboarding.R import com.tangem.feature.onboarding.R
import com.tangem.feature.onboarding.presentation.wallet2.model.ButtonState import com.tangem.feature.onboarding.presentation.wallet2.model.ButtonState
@ -38,33 +39,31 @@ import kotlinx.collections.immutable.persistentListOf
*/ */
@Composable @Composable
fun ImportSeedPhraseScreen(state: ImportSeedPhraseState, modifier: Modifier = Modifier) { fun ImportSeedPhraseScreen(state: ImportSeedPhraseState, modifier: Modifier = Modifier) {
Column( val keyboard by keyboardAsState()
Box(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
) { ) {
Box( Column {
modifier = Modifier.weight(1f), OnboardingDescriptionBlock(
) { modifier = Modifier.padding(top = TangemTheme.dimens.size16),
Column { ) {
OnboardingDescriptionBlock( DescriptionSubTitleText(text = stringResource(id = R.string.onboarding_seed_import_message))
modifier = Modifier.padding(top = TangemTheme.dimens.size16),
) {
DescriptionSubTitleText(text = stringResource(id = R.string.onboarding_seed_import_message))
}
PhraseBlock(
modifier = Modifier
.padding(top = TangemTheme.dimens.size62)
.padding(horizontal = TangemTheme.dimens.size16),
state = state,
)
SuggestionsBlock(
modifier = Modifier.padding(top = TangemTheme.dimens.size4),
suggestionsList = state.suggestionsList,
onClick = { index -> state.onSuggestedPhraseClick(index) },
)
} }
} PhraseBlock(
Box { modifier = Modifier
.padding(top = TangemTheme.dimens.size62)
.padding(horizontal = TangemTheme.dimens.size16),
state = state,
)
PassphraseBlock(
modifier = Modifier.padding(horizontal = TangemTheme.dimens.size16),
passphraseField = state.fieldPassphrase,
onPassphraseInfoClick = state.onPassphraseInfoClick,
)
OnboardingActionBlock( OnboardingActionBlock(
modifier = Modifier.padding(top = TangemTheme.dimens.size16),
firstActionContent = { firstActionContent = {
PrimaryButtonIconEnd( PrimaryButtonIconEnd(
modifier = Modifier modifier = Modifier
@ -78,9 +77,41 @@ fun ImportSeedPhraseScreen(state: ImportSeedPhraseState, modifier: Modifier = Mo
}, },
) )
} }
if (keyboard is Keyboard.Opened) {
SuggestionsBlock(
modifier = Modifier
.align(Alignment.BottomCenter)
.imePadding(),
suggestionsList = state.suggestionsList,
onClick = { index -> state.onSuggestedPhraseClick(index) },
)
}
if (state.bottomSheetConfig != null) {
PassphraseInfoBottomSheet(config = state.bottomSheetConfig)
}
} }
} }
@Composable
private fun PassphraseBlock(
passphraseField: TextFieldState,
onPassphraseInfoClick: () -> Unit,
modifier: Modifier = Modifier,
) {
OutlineTextFieldWithIcon(
modifier = modifier.fillMaxWidth(),
value = passphraseField.textFieldValue,
onValueChange = passphraseField.onTextFieldValueChanged,
iconResId = R.drawable.ic_information_24,
iconColor = TangemTheme.colors.icon.informative,
label = stringResource(id = R.string.common_passphrase),
placeholder = stringResource(id = R.string.send_optional_field),
onIconClick = onPassphraseInfoClick,
)
}
@Composable @Composable
private fun PhraseBlock(state: ImportSeedPhraseState, modifier: Modifier = Modifier) { private fun PhraseBlock(state: ImportSeedPhraseState, modifier: Modifier = Modifier) {
// TODO: replace by TextReference // TODO: replace by TextReference
@ -93,8 +124,8 @@ private fun PhraseBlock(state: ImportSeedPhraseState, modifier: Modifier = Modif
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(TangemTheme.dimens.size142), .height(TangemTheme.dimens.size142),
value = state.tvSeedPhrase.textFieldValue, value = state.fieldSeedPhrase.textFieldValue,
onValueChange = state.tvSeedPhrase.onTextFieldValueChanged, onValueChange = state.fieldSeedPhrase.onTextFieldValueChanged,
textStyle = TangemTheme.typography.body1, textStyle = TangemTheme.typography.body1,
singleLine = false, singleLine = false,
visualTransformation = InvalidWordsColorTransformation( visualTransformation = InvalidWordsColorTransformation(
@ -131,12 +162,12 @@ private fun SuggestionsBlock(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
AnimatedVisibility( AnimatedVisibility(
modifier = modifier,
enter = fadeIn() + slideIn(initialOffset = { IntOffset(x = 200, y = 0) }), enter = fadeIn() + slideIn(initialOffset = { IntOffset(x = 200, y = 0) }),
exit = slideOut(targetOffset = { IntOffset(x = -200, y = 0) }) + fadeOut(), exit = slideOut(targetOffset = { IntOffset(x = -200, y = 0) }) + fadeOut(),
visible = suggestionsList.isNotEmpty(), visible = suggestionsList.isNotEmpty(),
) { ) {
LazyRow( LazyRow(
modifier = modifier,
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.size16), contentPadding = PaddingValues(horizontal = TangemTheme.dimens.size16),
) { ) {
items(suggestionsList.size) { index -> items(suggestionsList.size) { index ->
@ -160,7 +191,7 @@ private fun SuggestionButton(text: String, onClick: () -> Unit, modifier: Modifi
Box(modifier = modifier.clickable(onClick = onClick)) { Box(modifier = modifier.clickable(onClick = onClick)) {
Notifier( Notifier(
text = text, text = text,
backgroundColor = TangemTheme.colors.background.action, backgroundColor = TangemTheme.colors.icon.primary1,
textColor = TangemTheme.colors.text.primary2, textColor = TangemTheme.colors.text.primary2,
) )
} }
@ -204,19 +235,23 @@ private fun ImportSeedPhraseScreenPreview_Light(
@PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList<String>, @PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList<String>,
) { ) {
TangemTheme(isDark = false) { TangemTheme(isDark = false) {
ImportSeedPhraseScreen( Box(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
ImportSeedPhraseState( ImportSeedPhraseScreen(
tvSeedPhrase = TextFieldState(onTextFieldValueChanged = {}), ImportSeedPhraseState(
onSuggestedPhraseClick = {}, fieldSeedPhrase = TextFieldState(onTextFieldValueChanged = {}),
buttonCreateWallet = ButtonState( fieldPassphrase = TextFieldState(onTextFieldValueChanged = {}),
enabled = true, onSuggestedPhraseClick = {},
isClickable = true, onPassphraseInfoClick = {},
showProgress = false, buttonCreateWallet = ButtonState(
onClick = {}, enabled = true,
isClickable = true,
showProgress = false,
onClick = {},
),
suggestionsList = suggestions,
), ),
suggestionsList = suggestions, )
), }
)
} }
} }

View file

@ -0,0 +1,89 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.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.res.TangemTheme
import com.tangem.feature.onboarding.R
import com.tangem.feature.onboarding.presentation.wallet2.model.ShowPassphraseInfoBottomSheetContent
@Composable
fun PassphraseInfoBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet(
config = config,
containerColor = TangemTheme.colors.background.primary,
) { content: ShowPassphraseInfoBottomSheetContent ->
PassphraseInfoBottomSheetContent(content)
}
}
@Composable
fun PassphraseInfoBottomSheetContent(content: ShowPassphraseInfoBottomSheetContent) {
Column(
modifier = Modifier
.background(color = TangemTheme.colors.background.primary)
.fillMaxWidth(),
) {
Icon(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(top = TangemTheme.dimens.size40)
.size(TangemTheme.dimens.size48),
painter = painterResource(id = R.drawable.ic_information_24),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
Text(
text = 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 = content.onOkClick,
)
}
}
@Preview
@Composable
private fun PassphraseInfoBottomSheetContentPreview() {
TangemTheme {
PassphraseInfoBottomSheetContent(ShowPassphraseInfoBottomSheetContent { })
}
}

View file

@ -2,9 +2,11 @@ package com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders
import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextRange
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.feature.onboarding.domain.InsertSuggestionResult import com.tangem.feature.onboarding.domain.InsertSuggestionResult
import com.tangem.feature.onboarding.domain.SeedPhraseError import com.tangem.feature.onboarding.domain.SeedPhraseError
import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState
import com.tangem.feature.onboarding.presentation.wallet2.model.ShowPassphraseInfoBottomSheetContent
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
/** /**
@ -12,12 +14,23 @@ import kotlinx.collections.immutable.ImmutableList
*/ */
class ImportSeedPhraseStateBuilder { class ImportSeedPhraseStateBuilder {
fun updateTextField( fun updateSeedPhraseTextField(
uiState: OnboardingSeedPhraseState, uiState: OnboardingSeedPhraseState,
textFieldValue: TextFieldValue, textFieldValue: TextFieldValue,
): OnboardingSeedPhraseState = uiState.copy( ): OnboardingSeedPhraseState = uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy( importSeedPhraseState = uiState.importSeedPhraseState.copy(
tvSeedPhrase = uiState.importSeedPhraseState.tvSeedPhrase.copy( fieldSeedPhrase = uiState.importSeedPhraseState.fieldSeedPhrase.copy(
textFieldValue = textFieldValue,
),
),
)
fun updatePassPhraseTextField(
uiState: OnboardingSeedPhraseState,
textFieldValue: TextFieldValue,
): OnboardingSeedPhraseState = uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
fieldPassphrase = uiState.importSeedPhraseState.fieldPassphrase.copy(
textFieldValue = textFieldValue, textFieldValue = textFieldValue,
), ),
), ),
@ -49,15 +62,40 @@ class ImportSeedPhraseStateBuilder {
), ),
) )
fun showPassphraseInfoBottomSheet(
uiState: OnboardingSeedPhraseState,
onDismiss: () -> Unit,
): OnboardingSeedPhraseState {
return uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
bottomSheetConfig = TangemBottomSheetConfig(
isShow = true,
onDismissRequest = onDismiss,
content = ShowPassphraseInfoBottomSheetContent(onDismiss),
),
),
)
}
fun dismissPassphraseBottomSheet(uiState: OnboardingSeedPhraseState): OnboardingSeedPhraseState {
return uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
bottomSheetConfig = uiState.importSeedPhraseState.bottomSheetConfig?.copy(
isShow = false,
),
),
)
}
fun insertSuggestionWord( fun insertSuggestionWord(
uiState: OnboardingSeedPhraseState, uiState: OnboardingSeedPhraseState,
insertResult: InsertSuggestionResult, insertResult: InsertSuggestionResult,
): OnboardingSeedPhraseState { ): OnboardingSeedPhraseState {
val newTextFieldValue = uiState.importSeedPhraseState.tvSeedPhrase.textFieldValue.copy( val newTextFieldValue = uiState.importSeedPhraseState.fieldSeedPhrase.textFieldValue.copy(
text = insertResult.text, text = insertResult.text,
selection = TextRange(insertResult.cursorPosition, insertResult.cursorPosition), selection = TextRange(insertResult.cursorPosition, insertResult.cursorPosition),
) )
return updateTextField(uiState, newTextFieldValue) return updateSeedPhraseTextField(uiState, newTextFieldValue)
} }
fun updateError(uiState: OnboardingSeedPhraseState, error: SeedPhraseError?): OnboardingSeedPhraseState { fun updateError(uiState: OnboardingSeedPhraseState, error: SeedPhraseError?): OnboardingSeedPhraseState {

View file

@ -28,11 +28,16 @@ class StateBuilder(
private fun getInitialImportSeedPhraseState(): ImportSeedPhraseState { private fun getInitialImportSeedPhraseState(): ImportSeedPhraseState {
return ImportSeedPhraseState( return ImportSeedPhraseState(
tvSeedPhrase = TextFieldState( fieldSeedPhrase = TextFieldState(
onTextFieldValueChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onTextFieldChanged, onTextFieldValueChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onTextFieldChanged,
onFocusChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onFocusChanged, onFocusChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onFocusChanged,
), ),
fieldPassphrase = TextFieldState(
onTextFieldValueChanged = uiActions.importSeedPhraseActions.passTextFieldAction.onTextFieldChanged,
onFocusChanged = uiActions.importSeedPhraseActions.passTextFieldAction.onFocusChanged,
),
onSuggestedPhraseClick = uiActions.importSeedPhraseActions.suggestedPhraseClick, onSuggestedPhraseClick = uiActions.importSeedPhraseActions.suggestedPhraseClick,
onPassphraseInfoClick = uiActions.importSeedPhraseActions.onPassphraseInfoClick,
buttonCreateWallet = ButtonState( buttonCreateWallet = ButtonState(
enabled = false, enabled = false,
onClick = uiActions.importSeedPhraseActions.buttonCreateWalletClick, onClick = uiActions.importSeedPhraseActions.buttonCreateWalletClick,

View file

@ -11,6 +11,7 @@ interface SeedPhraseMediator {
fun createWallet(callback: (CompletionResult<CreateWalletResponse>) -> Unit) fun createWallet(callback: (CompletionResult<CreateWalletResponse>) -> Unit)
fun importWallet( fun importWallet(
mnemonicComponents: List<String>, mnemonicComponents: List<String>,
passphrase: String?,
seedPhraseSource: SeedPhraseSource, seedPhraseSource: SeedPhraseSource,
callback: (CompletionResult<CreateWalletResponse>) -> Unit, callback: (CompletionResult<CreateWalletResponse>) -> Unit,
) )

View file

@ -74,6 +74,7 @@ class SeedPhraseViewModel @Inject constructor(
private var generatedMnemonicComponents: Map<MnemonicType, List<String>> = emptyMap() private var generatedMnemonicComponents: Map<MnemonicType, List<String>> = emptyMap()
private var importedMnemonicComponents: List<String>? = null private var importedMnemonicComponents: List<String>? = null
private var importedPassphrase: String? = null
override fun onCleared() { override fun onCleared() {
textFieldsDebouncers.forEach { entry -> entry.value.release() } textFieldsDebouncers.forEach { entry -> entry.value.release() }
@ -143,7 +144,7 @@ class SeedPhraseViewModel @Inject constructor(
buttonCreateWalletClick = { buttonCreateWalletClick = {
val mnemonicComponents = val mnemonicComponents =
generatedMnemonicComponents[selectedMnemonicType] ?: return@CheckSeedPhraseUiAction generatedMnemonicComponents[selectedMnemonicType] ?: return@CheckSeedPhraseUiAction
buttonImportWalletClick(mnemonicComponents, SeedPhraseSource.GENERATED) buttonImportWalletClick(mnemonicComponents, null, SeedPhraseSource.GENERATED)
}, },
secondTextFieldAction = TextFieldUiAction( secondTextFieldAction = TextFieldUiAction(
onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Second, value) }, onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Second, value) },
@ -159,10 +160,14 @@ class SeedPhraseViewModel @Inject constructor(
phraseTextFieldAction = TextFieldUiAction( phraseTextFieldAction = TextFieldUiAction(
onTextFieldChanged = { value -> onSeedPhraseTextFieldChanged(value) }, onTextFieldChanged = { value -> onSeedPhraseTextFieldChanged(value) },
), ),
passTextFieldAction = TextFieldUiAction(
onTextFieldChanged = { value -> onPassPhraseTextFieldChanged(value) },
),
suggestedPhraseClick = ::buttonSuggestedPhraseClick, suggestedPhraseClick = ::buttonSuggestedPhraseClick,
onPassphraseInfoClick = ::buttonPassphraseInfoClick,
buttonCreateWalletClick = { buttonCreateWalletClick = {
analyticsEventHandler.send(SeedPhraseEvents.ButtonImport) analyticsEventHandler.send(SeedPhraseEvents.ButtonImport)
buttonImportWalletClick(importedMnemonicComponents, SeedPhraseSource.IMPORTED) buttonImportWalletClick(importedMnemonicComponents, importedPassphrase, SeedPhraseSource.IMPORTED)
}, },
), ),
menuChatClick = ::menuChatClick, menuChatClick = ::menuChatClick,
@ -208,13 +213,13 @@ class SeedPhraseViewModel @Inject constructor(
// region ImportSeedPhrase // region ImportSeedPhrase
private fun onSeedPhraseTextFieldChanged(textFieldValue: TextFieldValue) { private fun onSeedPhraseTextFieldChanged(textFieldValue: TextFieldValue) {
val oldTextFieldValue = uiState.importSeedPhraseState.tvSeedPhrase.textFieldValue val oldTextFieldValue = uiState.importSeedPhraseState.fieldSeedPhrase.textFieldValue
val isSameText = textFieldValue.text == oldTextFieldValue.text val isSameText = textFieldValue.text == oldTextFieldValue.text
val isCursorMoved = textFieldValue.selection != oldTextFieldValue.selection val isCursorMoved = textFieldValue.selection != oldTextFieldValue.selection
uiState = uiBuilder.importSeedPhrase.updateTextField(uiState, textFieldValue) uiState = uiBuilder.importSeedPhrase.updateSeedPhraseTextField(uiState, textFieldValue)
val fieldState = uiState.importSeedPhraseState.tvSeedPhrase val fieldState = uiState.importSeedPhraseState.fieldSeedPhrase
val inputMnemonic = fieldState.textFieldValue.text val inputMnemonic = fieldState.textFieldValue.text
val debouncer = createOrGetDebouncer(MNEMONIC_DEBOUNCER) val debouncer = createOrGetDebouncer(MNEMONIC_DEBOUNCER)
@ -236,6 +241,11 @@ class SeedPhraseViewModel @Inject constructor(
} }
} }
private fun onPassPhraseTextFieldChanged(textFieldValue: TextFieldValue) {
importedPassphrase = textFieldValue.text
uiState = uiBuilder.importSeedPhrase.updatePassPhraseTextField(uiState, textFieldValue)
}
private suspend fun validateMnemonic(inputMnemonic: String) { private suspend fun validateMnemonic(inputMnemonic: String) {
if (inputMnemonic.isEmpty() && uiState.importSeedPhraseState.error != null) { if (inputMnemonic.isEmpty() && uiState.importSeedPhraseState.error != null) {
val mediateState = uiBuilder.importSeedPhrase.updateCreateWalletButton(uiState, enabled = false) val mediateState = uiBuilder.importSeedPhrase.updateCreateWalletButton(uiState, enabled = false)
@ -284,11 +294,15 @@ class SeedPhraseViewModel @Inject constructor(
} }
} }
private fun buttonImportWalletClick(mnemonicComponents: List<String>?, seedPhraseSource: SeedPhraseSource) { private fun buttonImportWalletClick(
mnemonicComponents: List<String>?,
passphrase: String?,
seedPhraseSource: SeedPhraseSource,
) {
mnemonicComponents ?: return mnemonicComponents ?: return
viewModelScope.launch(dispatchers.io) { viewModelScope.launch(dispatchers.io) {
mediator.importWallet(mnemonicComponents, seedPhraseSource, ::handleWalletCreationResult) mediator.importWallet(mnemonicComponents, passphrase, seedPhraseSource, ::handleWalletCreationResult)
} }
} }
@ -391,7 +405,7 @@ class SeedPhraseViewModel @Inject constructor(
private fun buttonSuggestedPhraseClick(suggestionIndex: Int) { private fun buttonSuggestedPhraseClick(suggestionIndex: Int) {
launchSingle { launchSingle {
val textFieldValue = uiState.importSeedPhraseState.tvSeedPhrase.textFieldValue val textFieldValue = uiState.importSeedPhraseState.fieldSeedPhrase.textFieldValue
val word = uiState.importSeedPhraseState.suggestionsList[suggestionIndex] val word = uiState.importSeedPhraseState.suggestionsList[suggestionIndex]
val cursorPosition = textFieldValue.selection.end val cursorPosition = textFieldValue.selection.end
@ -406,6 +420,22 @@ class SeedPhraseViewModel @Inject constructor(
} }
} }
} }
private fun buttonPassphraseInfoClick() {
launchSingle {
updateUi {
uiBuilder.importSeedPhrase.showPassphraseInfoBottomSheet(uiState) {
dismissPassphraseBottomSheet()
}
}
}
}
private fun dismissPassphraseBottomSheet() {
launchSingle {
updateUi { uiBuilder.importSeedPhrase.dismissPassphraseBottomSheet(uiState) }
}
}
// endregion ButtonClickHandlers // endregion ButtonClickHandlers
// region Utils // region Utils