Merge pull request #605 from dmnyc/feat/tx-history-sheet

feat(wallet): transaction history as swipe-up bottom sheet
This commit is contained in:
Barry Deen
2026-06-30 17:29:57 -04:00
committed by GitHub
@@ -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()