@@ -24,19 +24,17 @@ import androidx.compose.material3.OutlinedTextField
2424import androidx.compose.material3.Scaffold
2525import androidx.compose.material3.Text
2626import androidx.compose.runtime.Composable
27- import androidx.compose.runtime.derivedStateOf
27+ import androidx.compose.runtime.LaunchedEffect
2828import androidx.compose.runtime.getValue
29- import androidx.compose.runtime.mutableStateListOf
30- import androidx.compose.runtime.mutableStateOf
3129import androidx.compose.runtime.remember
32- import androidx.compose.runtime.rememberCoroutineScope
33- import androidx.compose.runtime.setValue
34- import androidx.compose.runtime.snapshots.SnapshotStateList
30+ import androidx.hilt.navigation.compose.hiltViewModel
31+ import androidx.lifecycle.compose.collectAsStateWithLifecycle
3532import androidx.compose.ui.Alignment
3633import androidx.compose.ui.Modifier
3734import androidx.compose.ui.focus.onFocusChanged
3835import androidx.compose.ui.layout.onGloballyPositioned
3936import androidx.compose.ui.layout.positionInParent
37+ import androidx.compose.ui.platform.LocalFocusManager
4038import androidx.compose.ui.platform.LocalSoftwareKeyboardController
4139import androidx.compose.ui.platform.testTag
4240import androidx.compose.ui.res.stringResource
@@ -58,60 +56,35 @@ import to.bitkit.ui.theme.AppTextFieldDefaults
5856import to.bitkit.ui.theme.AppThemeSurface
5957import to.bitkit.ui.theme.Colors
6058import to.bitkit.ui.utils.withAccent
61- import to.bitkit.utils.bip39Words
62- import to.bitkit.utils.isBip39
63- import to.bitkit.utils.validBip39Checksum
59+ import to.bitkit.viewmodels.RestoreWalletViewModel
6460
6561@Composable
6662fun RestoreWalletView (
63+ viewModel : RestoreWalletViewModel = hiltViewModel(),
6764 onBackClick : () -> Unit ,
6865 onRestoreClick : (mnemonic: String , passphrase: String? ) -> Unit ,
6966) {
70- val words = remember { mutableStateListOf(* Array (24 ) { " " }) }
71- val invalidWordIndices = remember { mutableStateListOf<Int >() }
72- val suggestions = remember { mutableStateListOf<String >() }
73- var focusedIndex by remember { mutableStateOf<Int ?>(null ) }
74- var bip39Passphrase by remember { mutableStateOf(" " ) }
75- var showingPassphrase by remember { mutableStateOf(false ) }
76- var firstFieldText by remember { mutableStateOf(" " ) }
77- var is24Words by remember { mutableStateOf(false ) }
78- val checksumErrorVisible by remember {
79- derivedStateOf {
80- val wordCount = if (is24Words) 24 else 12
81- words.subList(0 , wordCount).none { it.isBlank() } && invalidWordIndices.isEmpty() && ! words.subList(
82- 0 ,
83- wordCount
84- ).validBip39Checksum()
85- }
86- }
67+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
8768
8869 val scrollState = rememberScrollState()
89- val coroutineScope = rememberCoroutineScope()
90- val keyboardController = LocalSoftwareKeyboardController .current
9170 val inputFieldPositions = remember { mutableMapOf<Int , Int >() }
71+ val keyboardController = LocalSoftwareKeyboardController .current
72+ val focusManager = LocalFocusManager .current
9273
93- val wordsPerColumn = if (is24Words) 12 else 6
94-
95- val bip39Mnemonic by remember {
96- derivedStateOf {
97- val wordCount = if (is24Words) 24 else 12
98- words.subList(0 , wordCount)
99- .joinToString(separator = " " )
100- .trim()
74+ LaunchedEffect (uiState.shouldDismissKeyboard) {
75+ if (uiState.shouldDismissKeyboard) {
76+ focusManager.clearFocus()
77+ keyboardController?.hide()
78+ viewModel.onKeyboardDismissed()
10179 }
10280 }
10381
104- fun updateSuggestions (input : String , index : Int? ) {
105- if (index == null || input.length < 2 ) {
106- suggestions.clear()
107- return
108- }
109-
110- suggestions.clear()
111- if (input.isNotEmpty()) {
112- val filtered = bip39Words.filter { it.startsWith(input.lowercase()) }.take(3 )
113- if (filtered.size == 1 && filtered.firstOrNull() == input) return
114- suggestions.addAll(filtered)
82+ LaunchedEffect (uiState.scrollToFieldIndex) {
83+ uiState.scrollToFieldIndex?.let { index ->
84+ inputFieldPositions[index]?.let { position ->
85+ scrollState.animateScrollTo(position)
86+ }
87+ viewModel.onScrollCompleted()
11588 }
11689 }
11790
@@ -149,60 +122,14 @@ fun RestoreWalletView(
149122 verticalArrangement = Arrangement .spacedBy(4 .dp),
150123 modifier = Modifier .weight(1f )
151124 ) {
152- for (index in 0 until wordsPerColumn) {
125+ for (index in 0 until uiState. wordsPerColumn) {
153126 MnemonicInputField (
154127 label = " ${index + 1 } ." ,
155- value = if (index == 0 ) firstFieldText else words[index],
156- isError = index in invalidWordIndices,
157- onValueChanged = { newValue ->
158- if (index == 0 ) {
159- if (newValue.contains(" " )) {
160- handlePastedWords(
161- newValue,
162- words,
163- onWordCountChanged = { is24Words = it },
164- onFirstWordChanged = { firstFieldText = it },
165- onValidWords = { keyboardController?.hide() },
166- onInvalidWords = { invalidIndices ->
167- invalidWordIndices.clear()
168- invalidWordIndices.addAll(invalidIndices)
169- },
170- )
171- } else {
172- updateWordValidity(
173- newValue,
174- index,
175- words,
176- invalidWordIndices,
177- onWordUpdate = { firstFieldText = it }
178- )
179- updateSuggestions(newValue, focusedIndex)
180- }
181- } else {
182- updateWordValidity(
183- newValue,
184- index,
185- words,
186- invalidWordIndices,
187- )
188- updateSuggestions(newValue, focusedIndex)
189- }
190- coroutineScope.launch {
191- inputFieldPositions[index]?.let { scrollState.animateScrollTo(it) }
192- }
193- },
128+ value = uiState.words[index],
129+ isError = index in uiState.invalidWordIndices,
130+ onValueChanged = { viewModel.onWordChanged(index, it) },
194131 onFocusChanged = { focused ->
195- if (focused) {
196- focusedIndex = index
197- updateSuggestions(if (index == 0 ) firstFieldText else words[index], index)
198-
199- coroutineScope.launch {
200- inputFieldPositions[index]?.let { scrollState.animateScrollTo(it) }
201- }
202- } else if (focusedIndex == index) {
203- focusedIndex = null
204- suggestions.clear()
205- }
132+ viewModel.onWordFocusChanged(index, focused)
206133 },
207134 onPositionChanged = { position ->
208135 inputFieldPositions[index] = position
@@ -216,37 +143,14 @@ fun RestoreWalletView(
216143 verticalArrangement = Arrangement .spacedBy(4 .dp),
217144 modifier = Modifier .weight(1f )
218145 ) {
219- for (index in wordsPerColumn until (wordsPerColumn * 2 )) {
146+ for (index in uiState. wordsPerColumn until (uiState. wordsPerColumn * 2 )) {
220147 MnemonicInputField (
221148 label = " ${index + 1 } ." ,
222- value = words[index],
223- isError = index in invalidWordIndices,
224- onValueChanged = { newValue ->
225- words[index] = newValue
226-
227- updateWordValidity(
228- newValue,
229- index,
230- words,
231- invalidWordIndices,
232- )
233- updateSuggestions(newValue, focusedIndex)
234- coroutineScope.launch {
235- inputFieldPositions[index]?.let { scrollState.animateScrollTo(it) }
236- }
237- },
149+ value = uiState.words[index],
150+ isError = index in uiState.invalidWordIndices,
151+ onValueChanged = { viewModel.onWordChanged(index, it) },
238152 onFocusChanged = { focused ->
239- if (focused) {
240- focusedIndex = index
241- updateSuggestions(words[index], index)
242-
243- coroutineScope.launch {
244- inputFieldPositions[index]?.let { scrollState.animateScrollTo(it) }
245- }
246- } else if (focusedIndex == index) {
247- focusedIndex = null
248- suggestions.clear()
249- }
153+ viewModel.onWordFocusChanged(index, focused)
250154 },
251155 onPositionChanged = { position ->
252156 inputFieldPositions[index] = position
@@ -257,10 +161,10 @@ fun RestoreWalletView(
257161 }
258162 }
259163 // Passphrase
260- if (showingPassphrase) {
164+ if (uiState. showingPassphrase) {
261165 OutlinedTextField (
262- value = bip39Passphrase,
263- onValueChange = { bip39Passphrase = it },
166+ value = uiState. bip39Passphrase,
167+ onValueChange = { viewModel.onPassphraseChanged(it) },
264168 placeholder = {
265169 Text (
266170 text = stringResource(R .string.onboarding__restore_passphrase_placeholder)
@@ -293,7 +197,7 @@ fun RestoreWalletView(
293197 .weight(1f )
294198 )
295199
296- AnimatedVisibility (visible = invalidWordIndices.isNotEmpty()) {
200+ AnimatedVisibility (visible = uiState. invalidWordIndices.isNotEmpty()) {
297201 BodyS (
298202 text = stringResource(
299203 R .string.onboarding__restore_red_explain
@@ -303,7 +207,7 @@ fun RestoreWalletView(
303207 )
304208 }
305209
306- AnimatedVisibility (visible = checksumErrorVisible) {
210+ AnimatedVisibility (visible = uiState. checksumErrorVisible) {
307211 BodyS (
308212 text = stringResource(R .string.onboarding__restore_inv_checksum),
309213 color = Colors .Red ,
@@ -317,21 +221,11 @@ fun RestoreWalletView(
317221 .padding(vertical = 16 .dp)
318222 .fillMaxWidth(),
319223 ) {
320- val areButtonsEnabled by remember {
321- derivedStateOf {
322- val wordCount = if (is24Words) 24 else 12
323- words.subList(0 , wordCount)
324- .none { it.isBlank() } && invalidWordIndices.isEmpty() && ! checksumErrorVisible
325- }
326- }
327- AnimatedVisibility (visible = ! showingPassphrase, modifier = Modifier .weight(1f )) {
224+ AnimatedVisibility (visible = ! uiState.showingPassphrase, modifier = Modifier .weight(1f )) {
328225 SecondaryButton (
329226 text = stringResource(R .string.onboarding__advanced),
330- onClick = {
331- showingPassphrase = ! showingPassphrase
332- bip39Passphrase = " "
333- },
334- enabled = areButtonsEnabled,
227+ onClick = { viewModel.onAdvancedClick() },
228+ enabled = uiState.areButtonsEnabled,
335229 modifier = Modifier
336230 .weight(1f )
337231 .testTag(" AdvancedButton" )
@@ -340,9 +234,9 @@ fun RestoreWalletView(
340234 PrimaryButton (
341235 text = stringResource(R .string.onboarding__restore),
342236 onClick = {
343- onRestoreClick(bip39Mnemonic, bip39Passphrase.takeIf { it.isNotEmpty() })
237+ onRestoreClick(uiState. bip39Mnemonic, uiState. bip39Passphrase.takeIf { it.isNotEmpty() })
344238 },
345- enabled = areButtonsEnabled,
239+ enabled = uiState. areButtonsEnabled,
346240 modifier = Modifier
347241 .weight(1f )
348242 .testTag(" RestoreButton" )
@@ -352,7 +246,7 @@ fun RestoreWalletView(
352246
353247 // Suggestions row
354248 AnimatedVisibility (
355- visible = suggestions.isNotEmpty(),
249+ visible = uiState. suggestions.isNotEmpty(),
356250 enter = fadeIn() + expandVertically(),
357251 exit = fadeOut() + shrinkVertically(),
358252 modifier = Modifier
@@ -376,31 +270,10 @@ fun RestoreWalletView(
376270 .fillMaxWidth()
377271 .padding(top = 12 .dp)
378272 ) {
379- suggestions.forEach { suggestion ->
273+ uiState. suggestions.forEach { suggestion ->
380274 PrimaryButton (
381275 text = suggestion,
382- onClick = {
383- focusedIndex?.let { index ->
384- if (index == 0 ) {
385- firstFieldText = suggestion
386- updateWordValidity(
387- suggestion,
388- index,
389- words,
390- invalidWordIndices,
391- onWordUpdate = { firstFieldText = it }
392- )
393- } else {
394- updateWordValidity(
395- suggestion,
396- index,
397- words,
398- invalidWordIndices,
399- )
400- }
401- suggestions.clear()
402- }
403- },
276+ onClick = { viewModel.onSuggestionSelected(suggestion) },
404277 size = ButtonSize .Small ,
405278 fullWidth = false
406279 )
@@ -452,56 +325,6 @@ fun MnemonicInputField(
452325 )
453326}
454327
455- private fun handlePastedWords (
456- pastedText : String ,
457- words : SnapshotStateList <String >,
458- onWordCountChanged : (Boolean ) -> Unit ,
459- onFirstWordChanged : (String ) -> Unit ,
460- onInvalidWords : (List <Int >) -> Unit ,
461- onValidWords : () -> Unit ,
462- ) {
463- val pastedWords = pastedText.trim().split(" \\ s+" .toRegex()).filter { it.isNotEmpty() }
464- if (pastedWords.size == 12 || pastedWords.size == 24 ) {
465- val invalidWordIndices = pastedWords.withIndex()
466- .filter { ! it.value.isBip39() }
467- .map { it.index }
468-
469- if (invalidWordIndices.isNotEmpty()) {
470- onInvalidWords(invalidWordIndices)
471- }
472-
473- onWordCountChanged(pastedWords.size == 24 )
474- for (index in pastedWords.indices) {
475- words[index] = pastedWords[index]
476- }
477- for (index in pastedWords.size until words.size) {
478- words[index] = " "
479- }
480- onFirstWordChanged(pastedWords.first())
481- onValidWords()
482- }
483- }
484-
485- private fun updateWordValidity (
486- newValue : String ,
487- index : Int ,
488- words : SnapshotStateList <String >,
489- invalidWordIndices : SnapshotStateList <Int >,
490- onWordUpdate : ((String ) -> Unit )? = null,
491- ) {
492- words[index] = newValue
493- onWordUpdate?.invoke(newValue)
494-
495- val isValid = newValue.isBip39()
496- if (! isValid && newValue.isNotEmpty()) {
497- if (! invalidWordIndices.contains(index)) {
498- invalidWordIndices.add(index)
499- }
500- } else {
501- invalidWordIndices.remove(index)
502- }
503- }
504-
505328@Preview(showSystemUi = true )
506329@Composable
507330fun RestoreWalletViewPreview () {
0 commit comments