From ea6d03b29691a89206a1e5e777980cf1fd5b1f1a Mon Sep 17 00:00:00 2001 From: The Daniel Date: Tue, 30 Jun 2026 09:00:00 -0400 Subject: [PATCH] feat(wallet): transaction history as swipe-up bottom sheet The Transactions page now overlays the home dashboard as a ModalBottomSheet rather than replacing it. Swipe up 40px on the recent transactions footer or tap "View all" to open; swipe down or tap the scrim to dismiss. --- .../com/wisp/app/ui/screen/WalletScreen.kt | 85 ++++++++++++++++--- 1 file changed, 74 insertions(+), 11 deletions(-) diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/WalletScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/WalletScreen.kt index fdb9fdc..4656eef 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/WalletScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/WalletScreen.kt @@ -55,6 +55,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.gestures.detectVerticalDragGestures import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -115,11 +116,13 @@ import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -143,6 +146,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat @@ -204,6 +208,7 @@ fun WalletScreen( // app bar to match iOS — leaves more headroom for the centered logo // + title layout. val hideAppBar = currentPage is WalletPage.Home || + currentPage is WalletPage.Transactions || currentPage is WalletPage.ModeSelection || currentPage is WalletPage.NwcSetup || currentPage is WalletPage.SparkSetup || @@ -482,17 +487,36 @@ fun WalletScreen( ) } is WalletPage.Transactions -> { - // Observe profile refresh key so UI recomposes when new profiles arrive + // Home stays visible behind the transactions bottom sheet. val profileKey = viewModel.profileRefreshKey.collectAsState().value - TransactionHistoryContent( - transactions = viewModel.transactions.collectAsState().value, - error = viewModel.transactionsError.collectAsState().value, - isLoading = viewModel.isLoading.collectAsState().value, - isLoadingMore = viewModel.isLoadingMore.collectAsState().value, - hasMore = viewModel.hasMoreTransactions.collectAsState().value, - onLoadMore = { viewModel.loadMoreTransactions() }, - profileLookup = { viewModel.getProfileData(it) }, - profileRefreshKey = profileKey, + WalletHomeContent( + balanceMsats = balanceMsats, + walletMode = viewModel.walletMode.collectAsState().value, + balanceUnit = viewModel.balanceUnit.collectAsState().value, + showSettingsAlert = viewModel.walletMode.collectAsState().value == WalletMode.SPARK + && !viewModel.seedBackupAcked.collectAsState().value + && !viewModel.isDefaultWallet.collectAsState().value, + seedBackupAcked = viewModel.seedBackupAcked.collectAsState().value, + backupMissing = viewModel.backupMissing.collectAsState().value, + isDefaultWallet = viewModel.isDefaultWallet.collectAsState().value, + onSend = { viewModel.navigateTo(WalletPage.SendInput) }, + onReceive = { viewModel.navigateTo(WalletPage.ReceiveAmount) }, + onTransactions = {}, + onRefresh = { viewModel.refreshBalance() }, + onSettings = { viewModel.navigateTo(WalletPage.Settings) }, + onBackupToRelay = { + viewModel.resetBackupStatus() + viewModel.navigateTo(WalletPage.BackupToRelay) + }, + onViewSeed = { viewModel.showMnemonicBackup() }, + lightningAddress = viewModel.lightningAddress.collectAsState().value, + onSetupAddress = { + viewModel.resetAddressSetupState() + viewModel.navigateTo(WalletPage.LightningAddressSetup) + }, + recentTransactions = viewModel.transactions.collectAsState().value, + profileLookup = remember(profileKey) { { viewModel.getProfileData(it) } }, + nwcNodeAlias = viewModel.nwcNodeAlias.collectAsState().value, pubkey = viewModel.keyRepo.getPubkeyHex(), modifier = Modifier.padding(padding) ) @@ -612,6 +636,28 @@ fun WalletScreen( ) } } + + // Transactions full-screen bottom sheet — swipe down to dismiss + if (currentPage is WalletPage.Transactions) { + val txSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val txProfileKey = viewModel.profileRefreshKey.collectAsState().value + ModalBottomSheet( + onDismissRequest = { viewModel.navigateBack() }, + sheetState = txSheetState, + ) { + TransactionHistoryContent( + transactions = viewModel.transactions.collectAsState().value, + error = viewModel.transactionsError.collectAsState().value, + isLoading = viewModel.isLoading.collectAsState().value, + isLoadingMore = viewModel.isLoadingMore.collectAsState().value, + hasMore = viewModel.hasMoreTransactions.collectAsState().value, + onLoadMore = { viewModel.loadMoreTransactions() }, + profileLookup = { viewModel.getProfileData(it) }, + profileRefreshKey = txProfileKey, + pubkey = viewModel.keyRepo.getPubkeyHex(), + ) + } + } } } } @@ -1472,7 +1518,24 @@ private fun WalletHomeContent( // ── Recent transactions inline footer ────────────────────── if (recentTransactions.isNotEmpty()) { - Column(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier.fillMaxWidth() + .pointerInput(onTransactions) { + var totalDrag = 0f + detectVerticalDragGestures( + onDragStart = { totalDrag = 0f }, + onDragEnd = { totalDrag = 0f }, + onDragCancel = { totalDrag = 0f } + ) { change, delta -> + totalDrag += delta + if (totalDrag < -40f) { + totalDrag = 0f + change.consume() + onTransactions() + } + } + } + ) { Row( modifier = Modifier .fillMaxWidth()