diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/ManualBackupCheckComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/ManualBackupCheckComponent.kt new file mode 100644 index 0000000000..dfaf6fe787 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/ManualBackupCheckComponent.kt @@ -0,0 +1,39 @@ +package com.tangem.features.hotwallet.manualbackup.check + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.crypto.bip39.Mnemonic +import com.tangem.features.hotwallet.manualbackup.check.model.ManualBackupCheckModel +import com.tangem.features.hotwallet.manualbackup.check.ui.ManualBackupCheckContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject + +internal class ManualBackupCheckComponent @AssistedInject constructor( + @Assisted private val context: AppComponentContext, + @Assisted private val params: Params, +) : ComposableContentComponent, AppComponentContext by context { + private val model: ManualBackupCheckModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + ManualBackupCheckContent( + state = state, + modifier = modifier, + ) + } + + interface ModelCallbacks { + fun onCompleteClick() + } + + data class Params( + val generatedWords: Mnemonic, + val callbacks: ModelCallbacks, + ) +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/entity/ManualBackupCheckUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/entity/ManualBackupCheckUM.kt new file mode 100644 index 0000000000..19eac1ac68 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/entity/ManualBackupCheckUM.kt @@ -0,0 +1,18 @@ +package com.tangem.features.hotwallet.manualbackup.check.entity + +import androidx.compose.ui.text.input.TextFieldValue +import kotlinx.collections.immutable.ImmutableList + +internal data class ManualBackupCheckUM( + val onCompleteButtonClick: () -> Unit, + val wordFields: ImmutableList, + val completeButtonEnabled: Boolean, + val completeButtonProgress: Boolean, +) { + data class WordField( + val index: Int, + val word: TextFieldValue, + val error: Boolean, + val onChange: (TextFieldValue) -> Unit, + ) +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/model/ManualBackupCheckModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/model/ManualBackupCheckModel.kt new file mode 100644 index 0000000000..ffe2a0ad15 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/model/ManualBackupCheckModel.kt @@ -0,0 +1,105 @@ +package com.tangem.features.hotwallet.manualbackup.check.model + +import androidx.compose.runtime.Stable +import androidx.compose.ui.text.input.TextFieldValue +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.manualbackup.check.ManualBackupCheckComponent +import com.tangem.features.hotwallet.manualbackup.check.entity.ManualBackupCheckUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject +import kotlin.Boolean +import kotlin.Int +import kotlin.String +import kotlin.Suppress +import kotlin.collections.List +import kotlin.collections.all +import kotlin.collections.map +import kotlin.error + +@Stable +@ModelScoped +internal class ManualBackupCheckModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params = paramsContainer.require() + private val callbacks = params.callbacks + + internal val uiState: StateFlow + field = MutableStateFlow(getInitialUIState()) + + @Suppress("MagicNumber") + private fun getInitialUIState(): ManualBackupCheckUM { + val wordFields = List(WORD_FIELD_INDICES.size) { index -> + val shownIndex = WORD_FIELD_INDICES[index] + + ManualBackupCheckUM.WordField( + index = shownIndex, + word = TextFieldValue(""), + error = false, + onChange = { textFieldValue -> + updateWordField(shownIndex, textFieldValue) + }, + ) + }.toImmutableList() + + return ManualBackupCheckUM( + wordFields = wordFields, + completeButtonEnabled = false, + completeButtonProgress = false, + onCompleteButtonClick = { + val currentUIState = uiState.value + if (currentUIState.completeButtonEnabled) { + callbacks.onCompleteClick() + } + }, + ) + } + + private fun updateWordField(shownIndex: Int, newText: TextFieldValue) { + uiState.update { currentState -> + val updatedFields = currentState.wordFields.map { wordField -> + if (wordField.index == shownIndex) { + val isCorrect = checkWordField(newText.text, shownIndex) + wordField.copy( + word = newText, + error = !isCorrect, + ) + } else { + wordField + } + }.toImmutableList() + + val allFieldsCorrect = updatedFields.all { field -> + checkWordField(field.word.text, field.index) + } + + currentState.copy( + wordFields = updatedFields, + completeButtonEnabled = allFieldsCorrect, + ) + } + } + + private fun checkWordField(word: String, shownIndex: Int): Boolean { + val generatedWords = params.generatedWords + val wordList = generatedWords.mnemonicComponents + + return if (shownIndex <= wordList.size) { + wordList[shownIndex - 1] == word + } else { + false + } + } + + companion object { + private val WORD_FIELD_INDICES = listOf(2, 7, 11) + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/ui/ManualBackupCheckContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/ui/ManualBackupCheckContent.kt new file mode 100644 index 0000000000..04d1453435 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/ui/ManualBackupCheckContent.kt @@ -0,0 +1,147 @@ +package com.tangem.features.hotwallet.manualbackup.check.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.ImeAction +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 androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.core.ui.components.OutlineTextField +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.fields.contextmenu.DisableContextMenu +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.impl.R +import com.tangem.features.hotwallet.manualbackup.check.entity.ManualBackupCheckUM +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun ManualBackupCheckContent(state: ManualBackupCheckUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background(TangemTheme.colors.background.primary) + .fillMaxSize() + .imePadding(), + ) { + Column( + Modifier + .verticalScroll(rememberScrollState()) + .weight(1f), + ) { + Text( + text = stringResourceSafe(R.string.onboarding_seed_user_validation_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(top = 48.dp, start = 36.dp, end = 36.dp, bottom = 16.dp) + .fillMaxWidth(), + ) + Text( + text = stringResourceSafe(R.string.onboarding_seed_user_validation_message), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(horizontal = 36.dp) + .fillMaxWidth(), + ) + + Fields( + state = state, + modifier = Modifier + .padding(vertical = 30.dp, horizontal = 16.dp), + ) + } + + PrimaryButton( + modifier = Modifier + .padding(16.dp) + .imePadding() + .fillMaxWidth(), + text = stringResourceSafe(id = R.string.common_continue), + enabled = state.completeButtonEnabled, + showProgress = state.completeButtonProgress, + onClick = state.onCompleteButtonClick, + ) + } +} + +@Composable +private fun Fields(state: ManualBackupCheckUM, modifier: Modifier = Modifier) { + val focusManager = LocalFocusManager.current + + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + DisableContextMenu { + state.wordFields.fastForEachIndexed { index, field -> + OutlineTextField( + value = field.word, + onValueChange = field.onChange, + label = field.index.toString(), + isError = field.error, + keyboardOptions = KeyboardOptions( + autoCorrectEnabled = false, + keyboardType = KeyboardType.Password, + imeAction = if (index == state.wordFields.lastIndex) ImeAction.Done else ImeAction.Next, + ), + keyboardActions = KeyboardActions( + onDone = { focusManager.clearFocus() }, + onNext = { focusManager.moveFocus(focusDirection = FocusDirection.Down) }, + ), + ) + } + } + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + ManualBackupCheckContent( + state = ManualBackupCheckUM( + onCompleteButtonClick = {}, + wordFields = persistentListOf( + ManualBackupCheckUM.WordField( + index = 2, + word = TextFieldValue(text = "word"), + onChange = {}, + error = false, + ), + ManualBackupCheckUM.WordField( + index = 7, + word = TextFieldValue(text = "wor"), + onChange = {}, + error = true, + ), + ManualBackupCheckUM.WordField( + index = 11, + word = TextFieldValue(text = "word"), + onChange = {}, + error = false, + ), + ), + completeButtonEnabled = false, + completeButtonProgress = false, + ), + ) + } +} \ No newline at end of file