Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-22 12:33:37 +05:00
parent 5e1e708deb
commit b0b6081040
18 changed files with 669 additions and 55 deletions

View file

@ -15,4 +15,5 @@ object StringsSigns {
const val THREE_STARS = "\u2217\u2217\u2217"
const val PASSWORD_VISUAL_CHAR = '\u2022'
const val APPROXIMATE = ""
const val WHITE_SPACE = " "
}

View file

@ -17,6 +17,9 @@ dependencies {
implementation(projects.core.decompose)
implementation(projects.core.ui)
/* Tangem libraries */
implementation(tangemDeps.card.core)
/* Compose */
implementation(deps.compose.runtime)
}

View file

@ -0,0 +1,16 @@
package com.tangem.features.hotwallet
import com.tangem.crypto.bip39.Mnemonic
interface MnemonicRepository {
val words: Set<String>
fun generateMnemonic(type: MnemonicType = MnemonicType.Words12): Mnemonic
fun generateMnemonic(mnemonicString: String): Mnemonic
enum class MnemonicType {
Words12, Words24,
}
}

View file

@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.hotwallet.addexistingwallet.im.port.model.AddExistingWalletImportModel
import com.tangem.features.hotwallet.addexistingwallet.im.port.ui.AddExistingWalletImportContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject

View file

@ -1,26 +0,0 @@
package com.tangem.features.hotwallet.addexistingwallet.im.port
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.hotwallet.addexistingwallet.im.port.entity.AddExistingWalletImportUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@ModelScoped
internal class AddExistingWalletImportModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val params: AddExistingWalletImportComponent.Params = paramsContainer.require()
internal val uiState: StateFlow<AddExistingWalletImportUM>
field = MutableStateFlow(
AddExistingWalletImportUM(
onBackClick = params.callbacks::onBackClick,
),
)
}

View file

@ -1,5 +1,23 @@
package com.tangem.features.hotwallet.addexistingwallet.im.port.entity
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
internal data class AddExistingWalletImportUM(
val onBackClick: () -> Unit,
val words: TextFieldValue,
val wordsChange: (TextFieldValue) -> Unit,
val passPhrase: TextFieldValue,
val passPhraseChange: (TextFieldValue) -> Unit,
val onPassphraseInfoClick: () -> Unit,
val wordsErrorText: TextReference?,
val invalidWords: ImmutableList<String>,
val createWalletEnabled: Boolean,
val createWalletProgress: Boolean,
val createWalletClick: () -> Unit,
val suggestionsList: ImmutableList<String>,
val onSuggestionClick: (String) -> Unit,
val infoBottomSheetConfig: TangemBottomSheetConfig,
val readyToImport: Boolean,
)

View file

@ -0,0 +1,44 @@
package com.tangem.features.hotwallet.addexistingwallet.im.port.model
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.features.hotwallet.MnemonicRepository
import com.tangem.features.hotwallet.addexistingwallet.im.port.entity.AddExistingWalletImportUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
@ModelScoped
internal class AddExistingWalletImportModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val mnemonicRepository: MnemonicRepository,
) : Model() {
private val importSeedPhraseUiStateBuilder: ImportSeedPhraseUiStateBuilder
init {
importSeedPhraseUiStateBuilder = ImportSeedPhraseUiStateBuilder(
modelScope = modelScope,
mnemonicRepository = mnemonicRepository,
updateUiState = { block -> uiState.update { block(it) } },
readyToImport = { ready -> uiState.update { it.copy(readyToImport = ready) } },
importWallet = { mnemonic: Mnemonic, passphrase: String? ->
importWallet(
mnemonic = mnemonic,
passphrase = passphrase,
)
},
)
}
internal val uiState: StateFlow<AddExistingWalletImportUM>
field = MutableStateFlow(importSeedPhraseUiStateBuilder.getState())
@Suppress("UnusedPrivateMember")
private fun importWallet(mnemonic: Mnemonic, passphrase: String?) {
// TODO implement importing seed phrase
}
}

View file

@ -0,0 +1,188 @@
package com.tangem.features.hotwallet.addexistingwallet.im.port.model
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.common.core.TangemSdkError
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.crypto.bip39.MnemonicErrorResult
import com.tangem.features.hotwallet.MnemonicRepository
import com.tangem.features.hotwallet.addexistingwallet.im.port.entity.AddExistingWalletImportUM
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
internal class ImportSeedPhraseUiStateBuilder(
private val modelScope: CoroutineScope,
private val mnemonicRepository: MnemonicRepository,
private val readyToImport: (Boolean) -> Unit,
private val updateUiState: ((AddExistingWalletImportUM) -> AddExistingWalletImportUM) -> Unit,
private val importWallet: (mnemonic: Mnemonic, passphrase: String?) -> Unit,
) {
private val wordsCheckJobHolder = JobHolder()
private var importedMnemonic: Mnemonic? = null
private var passphrase: String? = null
fun getState(): AddExistingWalletImportUM {
return AddExistingWalletImportUM(
words = TextFieldValue(""),
passPhrase = TextFieldValue(""),
wordsErrorText = null,
invalidWords = persistentListOf(),
createWalletEnabled = false,
createWalletProgress = false,
suggestionsList = persistentListOf(),
wordsChange = {
launchInterceptWords(wordsField = it)
suggestNextWord(it)
updateUiState { state ->
state.copy(words = it)
}
},
passPhraseChange = {
passphrase = it.text
updateUiState { state -> state.copy(passPhrase = it) }
},
onPassphraseInfoClick = ::showInfoBS,
createWalletClick = ::onCreateWallet,
onSuggestionClick = { word -> addSuggestedWord(word) },
readyToImport = false,
infoBottomSheetConfig = TangemBottomSheetConfig.Empty,
)
}
private fun onCreateWallet() {
val mnemonic = importedMnemonic ?: return
val passphrase = passphrase?.takeIf { it.isNotEmpty() }
importWallet(mnemonic, passphrase)
}
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 < MINIMUM_WORD_LENGTH) 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 = WORDS_INTERCEPT_DELAY_MS)
interceptWords(wordsField)
}.saveIn(wordsCheckJobHolder)
}
private fun interceptWords(wordsField: TextFieldValue) {
readyToImport(false)
updateUiState {
it.copy(
createWalletEnabled = false,
wordsErrorText = null,
)
}
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)
importedMnemonic = mnemonic
updateUiState {
it.copy(
invalidWords = emptyList<String>().toImmutableList(),
wordsErrorText = null,
createWalletEnabled = true,
)
}
readyToImport(true)
} catch (ex: TangemSdkError.MnemonicException) {
readyToImport(false)
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.Companion.Empty.copy(
isShown = true,
onDismissRequest = {
updateUiState { it.copy(infoBottomSheetConfig = TangemBottomSheetConfig.Companion.Empty) }
},
),
)
}
}
companion object {
private const val MINIMUM_WORD_LENGTH = 2
private const val WORDS_INTERCEPT_DELAY_MS = 500L
}
}

View file

@ -1,36 +1,218 @@
package com.tangem.features.hotwallet.addexistingwallet.im.port.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideIn
import androidx.compose.animation.slideOut
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.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
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.text.SpanStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.hotwallet.addexistingwallet.im.port.entity.AddExistingWalletImportUM
import com.tangem.core.ui.R
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.Notifier
import com.tangem.core.ui.components.OutlineTextFieldWithIcon
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.TangemTextFieldsDefault
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.features.hotwallet.addexistingwallet.im.port.ui.utils.InvalidWordsColorTransformation
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun AddExistingWalletImportContent(state: AddExistingWalletImportUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary)
.fillMaxSize()
.systemBarsPadding(),
) {
TangemTopAppBar(
Box(modifier.fillMaxSize()) {
Column(
modifier = Modifier
.statusBarsPadding(),
startButton = TopAppBarButtonUM.Back(state.onBackClick),
title = stringResourceSafe(R.string.wallet_import_title),
.fillMaxSize()
.imePadding(),
) {
Column(
Modifier
.verticalScroll(rememberScrollState())
.weight(1f),
) {
Text(
text = stringResourceSafe(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 = stringResourceSafe(id = R.string.common_passphrase),
placeholder = stringResourceSafe(id = R.string.send_optional_field),
onIconClick = state.onPassphraseInfoClick,
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password,
),
)
}
PrimaryButtonIconEnd(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
text = stringResourceSafe(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: AddExistingWalletImportUM, 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.words,
onValueChange = state.wordsChange,
textStyle = TangemTheme.typography.body1,
singleLine = false,
colors = TangemTextFieldsDefault.defaultTextFieldColors,
visualTransformation = invalidWordsColorTransformation,
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password,
),
)
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(
modifier = Modifier.padding(bottom = 8.dp),
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) {
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)
rowSize - 1 -> this.padding(start = inSide, end = outSide)
else -> this.padding(horizontal = inSide)
}
@Preview(showBackground = true, widthDp = 360)
@ -40,7 +222,20 @@ private fun PreviewAddExistingWalletImportContent() {
TangemThemePreview {
AddExistingWalletImportContent(
state = AddExistingWalletImportUM(
onBackClick = {},
words = TextFieldValue(""),
wordsChange = {},
passPhrase = TextFieldValue(""),
passPhraseChange = {},
onPassphraseInfoClick = {},
wordsErrorText = null,
invalidWords = persistentListOf(),
createWalletEnabled = false,
createWalletProgress = false,
createWalletClick = {},
suggestionsList = persistentListOf(),
onSuggestionClick = {},
infoBottomSheetConfig = TangemBottomSheetConfig.Empty,
readyToImport = false,
),
)
}

View file

@ -0,0 +1,90 @@
package com.tangem.features.hotwallet.addexistingwallet.im.port.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@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 = stringResourceSafe(id = R.string.common_passphrase),
modifier = Modifier
.padding(top = TangemTheme.dimens.size40)
.align(Alignment.CenterHorizontally),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.h2,
)
Text(
text = stringResourceSafe(id = R.string.onboarding_bottom_sheet_passphrase_description),
modifier = Modifier
.padding(top = TangemTheme.dimens.size16)
.padding(horizontal = TangemTheme.dimens.size24)
.align(Alignment.CenterHorizontally),
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.body2,
textAlign = TextAlign.Center,
)
PrimaryButton(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.size16)
.padding(top = TangemTheme.dimens.size40)
.padding(bottom = TangemTheme.dimens.size32)
.fillMaxWidth(),
text = stringResourceSafe(id = R.string.common_ok),
onClick = onDismiss,
)
}
}
@Preview
@Composable
private fun PassphraseInfoBottomSheetContentPreview() {
TangemThemePreview {
PassphraseInfoBottomSheetContent({ })
}
}

View file

@ -0,0 +1,77 @@
package com.tangem.features.hotwallet.addexistingwallet.im.port.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.StringsSigns.WHITE_SPACE
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 WORD_SEPARATOR = "_$@-*"
}
}

View file

@ -6,7 +6,7 @@ import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeModel
import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel
import com.tangem.features.hotwallet.addexistingwallet.root.DefaultAddExistingWalletComponent
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartModel
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportModel
import com.tangem.features.hotwallet.addexistingwallet.im.port.model.AddExistingWalletImportModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn

View file

@ -1,22 +1,24 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model
package com.tangem.features.hotwallet.common.repository
import android.content.Context
import com.tangem.crypto.bip39.DefaultMnemonic
import com.tangem.crypto.bip39.EntropyLength
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.crypto.bip39.Wordlist
import com.tangem.features.hotwallet.MnemonicRepository
import com.tangem.features.hotwallet.MnemonicRepository.MnemonicType
import com.tangem.sdk.extensions.getWordlist
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
internal class MnemonicRepository @Inject constructor(
internal class DefaultMnemonicRepository @Inject constructor(
@ApplicationContext private val context: Context,
) {
) : MnemonicRepository {
private val wordlist = Wordlist.getWordlist(context)
val words: Set<String> = wordlist.words.toHashSet()
override val words: Set<String> = wordlist.words.toHashSet()
fun generateMnemonic(type: MnemonicType = MnemonicType.Words12): Mnemonic = DefaultMnemonic(
override fun generateMnemonic(type: MnemonicType): Mnemonic = DefaultMnemonic(
entropy = when (type) {
MnemonicType.Words12 -> EntropyLength.Bits128Length
MnemonicType.Words24 -> EntropyLength.Bits256Length
@ -24,12 +26,8 @@ internal class MnemonicRepository @Inject constructor(
wordlist = wordlist,
)
fun generateMnemonic(mnemonicString: String): Mnemonic = DefaultMnemonic(
override fun generateMnemonic(mnemonicString: String): Mnemonic = DefaultMnemonic(
mnemonic = mnemonicString,
wordlist = wordlist,
)
enum class MnemonicType {
Words12, Words24,
}
}

View file

@ -3,6 +3,9 @@ package com.tangem.features.hotwallet.di
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.hotwallet.DefaultHotWalletFeatureToggles
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.hotwallet.MnemonicRepository
import com.tangem.features.hotwallet.common.repository.DefaultMnemonicRepository
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -22,4 +25,8 @@ internal object HotWalletFeatureModule {
@Module
@InstallIn(SingletonComponent::class)
internal interface HotWalletFeatureModuleBinds
internal interface HotWalletFeatureModuleBinds {
@Binds
@Singleton
fun provideMnemonicRepository(repository: DefaultMnemonicRepository): MnemonicRepository
}

View file

@ -16,6 +16,7 @@ dependencies {
implementation(projects.features.onboardingV2.api)
implementation(projects.features.manageTokens.api)
implementation(projects.features.biometry.api)
implementation(projects.features.hotWallet.api)
/** Core modules */
implementation(projects.core.configToggles)

View file

@ -13,6 +13,7 @@ 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.hotwallet.MnemonicRepository
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams

View file

@ -7,8 +7,8 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.crypto.bip39.MnemonicErrorResult
import com.tangem.features.hotwallet.MnemonicRepository
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.ui.state.MultiWalletSeedPhraseUM
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn

View file

@ -6,6 +6,7 @@ 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.StringsSigns.WHITE_SPACE
import com.tangem.utils.extensions.isSingleItem
import kotlinx.collections.immutable.ImmutableList
@ -71,7 +72,6 @@ class InvalidWordsColorTransformation(
private fun AnnotatedString.toTransformedText(): TransformedText = TransformedText(this, OffsetMapping.Identity)
companion object {
private const val WHITE_SPACE = " "
private const val WORD_SEPARATOR = "_$@-*"
}
}