diff --git a/android/app/src/main/java/org/bitcoinppl/cove/flows/NewWalletFlow/hot_wallet/HotWalletImportCard.kt b/android/app/src/main/java/org/bitcoinppl/cove/flows/NewWalletFlow/hot_wallet/HotWalletImportCard.kt index 95b2f694..c7be7dd4 100644 --- a/android/app/src/main/java/org/bitcoinppl/cove/flows/NewWalletFlow/hot_wallet/HotWalletImportCard.kt +++ b/android/app/src/main/java/org/bitcoinppl/cove/flows/NewWalletFlow/hot_wallet/HotWalletImportCard.kt @@ -57,6 +57,7 @@ internal fun WordInputGrid( onWordsChanged: (List>) -> Unit, onFocusChanged: (Int) -> Unit, onPasteMnemonic: ((String) -> Unit)? = null, + modifier: Modifier = Modifier, ) { val wordCount = when (numberOfWords) { @@ -74,7 +75,7 @@ internal fun WordInputGrid( val rightIndices = (pageStart + wordsPerColumn until (pageStart + pageSize).coerceAtMost(wordCount)) Card( - modifier = Modifier.fillMaxWidth(), + modifier = modifier.fillMaxWidth(), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surfaceContainer, diff --git a/android/app/src/main/java/org/bitcoinppl/cove/flows/NewWalletFlow/hot_wallet/HotWalletImportScreen.kt b/android/app/src/main/java/org/bitcoinppl/cove/flows/NewWalletFlow/hot_wallet/HotWalletImportScreen.kt index ea2ddbdb..170089bc 100644 --- a/android/app/src/main/java/org/bitcoinppl/cove/flows/NewWalletFlow/hot_wallet/HotWalletImportScreen.kt +++ b/android/app/src/main/java/org/bitcoinppl/cove/flows/NewWalletFlow/hot_wallet/HotWalletImportScreen.kt @@ -1,5 +1,6 @@ package org.bitcoinppl.cove.flows.NewWalletFlow.hot_wallet +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -14,6 +15,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -39,6 +42,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -54,6 +58,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.bitcoinppl.cove.AppManager import org.bitcoinppl.cove.ImportWalletManager import org.bitcoinppl.cove.Log @@ -92,7 +97,7 @@ private fun HotWalletImportScreenPreview() { ) } -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun HotWalletImportScreen( app: AppManager, @@ -111,15 +116,29 @@ fun HotWalletImportScreen( } val numberOfGroups = wordCount / GROUPS_OF - var enteredWords by remember(currentNumberOfWords) { + var enteredWords by remember { mutableStateOf(List(numberOfGroups) { List(GROUPS_OF) { "" } }) } var alertState by remember { mutableStateOf(AlertState.None) } var duplicateWalletId by remember { mutableStateOf(null) } var genericErrorMessage by remember { mutableStateOf("") } - var focusedField by remember(currentNumberOfWords) { mutableIntStateOf(0) } - var tabIndex by remember(currentNumberOfWords) { mutableIntStateOf(0) } + var focusedField by remember { mutableIntStateOf(0) } + var tabIndex by remember { mutableIntStateOf(0) } + val pagerState = rememberPagerState(pageCount = { enteredWords.size }) + val scope = rememberCoroutineScope() + + // sync tabIndex from pager swipes + LaunchedEffect(pagerState.currentPage) { + tabIndex = pagerState.currentPage + } + + // scroll pager when tabIndex changes from focus + LaunchedEffect(tabIndex) { + if (pagerState.currentPage != tabIndex) { + pagerState.animateScrollToPage(tabIndex) + } + } // auto-switch page when focus changes to a word on a different page LaunchedEffect(focusedField, enteredWords) { @@ -328,21 +347,26 @@ fun HotWalletImportScreen( Column( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp), + .fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp), ) { Spacer(Modifier.height(24.dp)) - WordInputGrid( - enteredWords = enteredWords, - numberOfWords = currentNumberOfWords, - focusedField = focusedField, - tabIndex = tabIndex, - onWordsChanged = { newWords -> enteredWords = newWords }, - onFocusChanged = { field -> focusedField = field }, - onPasteMnemonic = ::handlePasteMnemonic, - ) + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxWidth(), + ) { page -> + WordInputGrid( + enteredWords = enteredWords, + numberOfWords = currentNumberOfWords, + focusedField = focusedField, + tabIndex = page, + onWordsChanged = { newWords -> enteredWords = newWords }, + onFocusChanged = { field -> focusedField = field }, + onPasteMnemonic = ::handlePasteMnemonic, + modifier = Modifier.padding(horizontal = 20.dp), + ) + } // page indicator dots for multi-page import if (enteredWords.size > 1) { @@ -365,7 +389,9 @@ fun HotWalletImportScreen( } else { Color.White.copy(alpha = 0.33f) }, - ).clickable { tabIndex = i }, + ).clickable { + scope.launch { pagerState.animateScrollToPage(i) } + }, ) } } diff --git a/ios/Cove/Flows/NewWalletFlow/HotWallet/HotWalletImportCard.swift b/ios/Cove/Flows/NewWalletFlow/HotWallet/HotWalletImportCard.swift index d3141121..f06637fa 100644 --- a/ios/Cove/Flows/NewWalletFlow/HotWallet/HotWalletImportCard.swift +++ b/ios/Cove/Flows/NewWalletFlow/HotWallet/HotWalletImportCard.swift @@ -244,8 +244,8 @@ private struct AutocompleteField: View { } } .onAppear { - if !text.isEmpty, autocomplete.isBip39Word(word: text) { - state = .valid + if !text.isEmpty { + state = autocomplete.isValidWord(word: text, allWords: allEnteredWords) ? .valid : .invalid } } .frame(maxWidth: .infinity) @@ -342,9 +342,17 @@ private struct AutocompleteField: View { textField.delegate = handler } } - .onChange(of: text, initial: false) { oldText, newText in + .onChange(of: text, initial: true) { oldText, newText in text = newText.trimmingCharacters(in: .whitespacesAndNewlines) + // handle programmatic text changes (e.g., paste) + if oldText == newText { + if !newText.isEmpty { + state = autocomplete.isValidWord(word: newText, allWords: allEnteredWords) ? .valid : .invalid + } + return + } + filteredSuggestions = autocomplete.autocomplete( word: newText, allWords: allEnteredWords )