diff --git a/android/app/src/main/java/org/bitcoinppl/cove/WalletManager.kt b/android/app/src/main/java/org/bitcoinppl/cove/WalletManager.kt index b4e9a939..0854b7ff 100644 --- a/android/app/src/main/java/org/bitcoinppl/cove/WalletManager.kt +++ b/android/app/src/main/java/org/bitcoinppl/cove/WalletManager.kt @@ -224,8 +224,21 @@ class WalletManager : } is WalletManagerReconcileMessage.AvailableTransactions -> { - if (loadState is WalletLoadState.LOADING) { - loadState = WalletLoadState.SCANNING(message.v1) + val txns = message.v1 + when (val current = loadState) { + is WalletLoadState.LOADING -> { + loadState = WalletLoadState.SCANNING(txns) + } + is WalletLoadState.SCANNING -> { + if (txns.size >= current.txns.size) { + loadState = WalletLoadState.SCANNING(txns) + } + } + is WalletLoadState.LOADED -> { + if (txns.size >= current.txns.size) { + loadState = WalletLoadState.SCANNING(txns) + } + } } } 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 9d2b6989..2035d02c 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 @@ -53,6 +53,7 @@ internal fun WordInputGrid( enteredWords: List>, numberOfWords: NumberOfBip39Words, focusedField: Int, + tabIndex: Int, onWordsChanged: (List>) -> Unit, onFocusChanged: (Int) -> Unit, ) { @@ -64,9 +65,12 @@ internal fun WordInputGrid( val flatWords = enteredWords.flatten() - val numRows = wordCount / 2 - val leftIndices = (0 until numRows) - val rightIndices = (numRows until wordCount) + // always show 12 words per page (6 per column) to match iOS pagination + val pageSize = 12 + val wordsPerColumn = 6 + val pageStart = tabIndex * pageSize + val leftIndices = (pageStart until pageStart + wordsPerColumn) + val rightIndices = (pageStart + wordsPerColumn until (pageStart + pageSize).coerceAtMost(wordCount)) Card( modifier = Modifier.fillMaxWidth(), 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 08b815f0..9424ef2a 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,6 +1,8 @@ package org.bitcoinppl.cove.flows.NewWalletFlow.hot_wallet import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -11,6 +13,8 @@ import androidx.compose.foundation.layout.fillMaxSize 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.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Nfc @@ -38,6 +42,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalFocusManager @@ -110,6 +115,15 @@ fun HotWalletImportScreen( var duplicateWalletId by remember { mutableStateOf(null) } var genericErrorMessage by remember { mutableStateOf("") } var focusedField by remember(numberOfWords) { mutableIntStateOf(0) } + var tabIndex by remember(numberOfWords) { mutableIntStateOf(0) } + + // auto-switch page when focus changes to a word on a different page + LaunchedEffect(focusedField) { + val newTab = focusedField / GROUPS_OF + if (newTab != tabIndex && newTab < numberOfGroups) { + tabIndex = newTab + } + } // QR and NFC state var showQrScanner by remember { mutableStateOf(false) } @@ -287,9 +301,37 @@ fun HotWalletImportScreen( enteredWords = enteredWords, numberOfWords = numberOfWords, focusedField = focusedField, + tabIndex = tabIndex, onWordsChanged = { newWords -> enteredWords = newWords }, onFocusChanged = { field -> focusedField = field }, ) + + // page indicator dots for 24-word import + if (numberOfWords == NumberOfBip39Words.TWENTY_FOUR) { + Spacer(Modifier.height(16.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + repeat(numberOfGroups) { i -> + val isSelected = i == tabIndex + Box( + modifier = + Modifier + .padding(horizontal = 4.dp) + .size(8.dp) + .clip(RoundedCornerShape(50)) + .background( + if (isSelected) { + Color.White + } else { + Color.White.copy(alpha = 0.33f) + }, + ).clickable { tabIndex = i }, + ) + } + } + } } Spacer(Modifier.weight(1f)) diff --git a/android/app/src/main/java/org/bitcoinppl/cove/flows/SelectedWalletFlow/TransactionsCardView.kt b/android/app/src/main/java/org/bitcoinppl/cove/flows/SelectedWalletFlow/TransactionsCardView.kt index 2d607111..b642d18a 100644 --- a/android/app/src/main/java/org/bitcoinppl/cove/flows/SelectedWalletFlow/TransactionsCardView.kt +++ b/android/app/src/main/java/org/bitcoinppl/cove/flows/SelectedWalletFlow/TransactionsCardView.kt @@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -52,8 +51,10 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.launch +import org.bitcoinppl.cove.AppAlertState import org.bitcoinppl.cove.AppManager import org.bitcoinppl.cove.R +import org.bitcoinppl.cove.TaggedItem import org.bitcoinppl.cove.WalletManager import org.bitcoinppl.cove.ui.theme.CoveColor import org.bitcoinppl.cove.ui.theme.isLight @@ -113,8 +114,8 @@ fun TransactionsCardView( fontWeight = FontWeight.Bold, ) - // small inline spinner when scanning with existing transactions - if (isScanning && hasTransactions) { + // show inline spinner when scanning, except during initial loading (first scan with no txns yet) + if (isScanning && !(isFirstScan && !hasTransactions)) { Box( modifier = Modifier @@ -148,16 +149,30 @@ fun TransactionsCardView( } } else { // scan complete but no transactions - Box( + Column( modifier = Modifier .fillMaxWidth() - .padding(vertical = 32.dp), - contentAlignment = Alignment.Center, + .padding(top = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { + Icon( + painter = + androidx.compose.ui.res + .painterResource(R.drawable.icon_currency_bitcoin), + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = secondaryText, + ) + Spacer(Modifier.height(8.dp)) Text( text = stringResource(R.string.no_transactions_yet), color = secondaryText, + fontWeight = FontWeight.Medium, + ) + Text( + text = stringResource(R.string.go_buy_some_bitcoin), + color = secondaryText.copy(alpha = 0.7f), fontSize = 14.sp, ) } @@ -185,7 +200,15 @@ fun TransactionsCardView( HorizontalDivider(color = dividerColor, thickness = 0.5.dp) } - itemsIndexed(transactions) { index, txn -> + items( + items = transactions, + key = { + when (it) { + is Transaction.Confirmed -> it.v1.id().toString() + is Transaction.Unconfirmed -> it.v1.id().toString() + } + }, + ) { txn -> TransactionItem( txn = txn, manager = manager, @@ -197,10 +220,7 @@ fun TransactionsCardView( secondaryText = secondaryText, ) - // add divider between transactions (but not after the last one) - if (index < transactions.size - 1) { - HorizontalDivider(color = dividerColor, thickness = 0.5.dp) - } + HorizontalDivider(color = dividerColor, thickness = 0.5.dp) } // add bottom spacing @@ -359,12 +379,15 @@ internal fun ConfirmedTransactionWidget( if (app != null && manager != null) { scope.launch { try { + app.alertState = TaggedItem(AppAlertState.Loading) val details = manager.transactionDetails(transaction.v1.id()) val walletId = manager.walletMetadata?.id + app.alertState = null if (walletId != null) { app.pushRoute(Route.TransactionDetails(walletId, details)) } } catch (e: Exception) { + app.alertState = null android.util.Log.e("ConfirmedTxWidget", "Failed to load transaction details", e) } } @@ -460,12 +483,15 @@ internal fun UnconfirmedTransactionWidget( if (app != null && manager != null) { scope.launch { try { + app.alertState = TaggedItem(AppAlertState.Loading) val details = manager.transactionDetails(transaction.v1.id()) val walletId = manager.walletMetadata?.id + app.alertState = null if (walletId != null) { app.pushRoute(Route.TransactionDetails(walletId, details)) } } catch (e: Exception) { + app.alertState = null android.util.Log.e("UnconfirmedTxWidget", "Failed to load transaction details", e) } } @@ -639,7 +665,7 @@ internal fun UnsignedTransactionWidget( ) Text( text = stringResource(R.string.pending_signature), - color = Color(0xFFFF9800), + color = Color(0xFFFF9800).copy(alpha = 0.8f), fontSize = 12.sp, fontWeight = FontWeight.Normal, ) @@ -648,7 +674,7 @@ internal fun UnsignedTransactionWidget( Column(horizontalAlignment = Alignment.End) { Text( text = privateShow(formattedAmount), - color = primaryText.copy(alpha = 0.6f), + color = primaryText, fontSize = 17.sp, fontWeight = FontWeight.Normal, ) diff --git a/android/app/src/main/java/org/bitcoinppl/cove/views/ImageButton.kt b/android/app/src/main/java/org/bitcoinppl/cove/views/ImageButton.kt index 2dfebf0c..9d3d8568 100644 --- a/android/app/src/main/java/org/bitcoinppl/cove/views/ImageButton.kt +++ b/android/app/src/main/java/org/bitcoinppl/cove/views/ImageButton.kt @@ -69,7 +69,7 @@ fun ImageButton( BasicText( text = text, maxLines = 1, - autoSize = TextAutoSize.StepBased(minFontSize = 7.sp, maxFontSize = fontSize, stepSize = 0.5.sp), + autoSize = TextAutoSize.StepBased(minFontSize = 12.sp, maxFontSize = fontSize, stepSize = 0.5.sp), style = TextStyle( fontSize = fontSize, diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 2ad790ef..eb575691 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -46,6 +46,7 @@ Receiving Transactions No transactions yet + Go buy some bitcoin! Pending Pending Signature Unconfirmed diff --git a/ios/Cove/WalletManager.swift b/ios/Cove/WalletManager.swift index 6d2ae8d8..c3a9ac3b 100644 --- a/ios/Cove/WalletManager.swift +++ b/ios/Cove/WalletManager.swift @@ -159,9 +159,13 @@ extension WeakReconciler: WalletManagerReconciler where Reconciler == WalletMana case let .availableTransactions(txns): switch self.loadState { - case .loading, .scanning: + case .loading: self.loadState = .scanning(txns) - case let .loaded(current) where txns.count > current.count: + case let .scanning(current) where txns.count >= current.count: + self.loadState = .scanning(txns) + case .scanning: + break + case let .loaded(current) where txns.count >= current.count: self.loadState = .scanning(txns) case .loaded: break