Updated on 2026-08-14
This commit is contained in:
parent
5e1e708deb
commit
b0b6081040
18 changed files with 669 additions and 55 deletions
|
|
@ -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 = " "
|
||||
}
|
||||
|
|
@ -17,6 +17,9 @@ dependencies {
|
|||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/* Tangem libraries */
|
||||
implementation(tangemDeps.card.core)
|
||||
|
||||
/* Compose */
|
||||
implementation(deps.compose.runtime)
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({ })
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = "_$@-*"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = "_$@-*"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue