fix: payment detail not loading on filter or search when not in memory

This commit is contained in:
2026-06-03 22:36:27 +02:00
parent 432cd0e478
commit cb9961608c
6 changed files with 21 additions and 37 deletions
@@ -72,7 +72,7 @@ sealed class TabItem(val route: String, val label: String, val icon: ImageVector
private const val ROUTE_HOME = "home" private const val ROUTE_HOME = "home"
private const val ROUTE_SCANNER = "scanner" private const val ROUTE_SCANNER = "scanner"
private const val ROUTE_HISTORY = "history" private const val ROUTE_HISTORY = "history"
private const val ROUTE_PAYMENT_DETAIL = "payment_detail/{paymentHash}" private const val ROUTE_PAYMENT_DETAIL = "payment_detail/{checkingId}"
@Composable @Composable
fun WalletScreen( fun WalletScreen(
@@ -317,7 +317,7 @@ fun WalletScreen(
navController.popBackStack(ROUTE_HOME, inclusive = false) navController.popBackStack(ROUTE_HOME, inclusive = false)
}, },
onPaymentClick = { payment -> onPaymentClick = { payment ->
navController.navigate("payment_detail/${payment.paymentHash}") navController.navigate("payment_detail/${payment.checkingId}")
} }
) )
} }
@@ -325,13 +325,12 @@ fun WalletScreen(
// Payment detail — back → history (natural back stack) // Payment detail — back → history (natural back stack)
composable( composable(
route = ROUTE_PAYMENT_DETAIL, route = ROUTE_PAYMENT_DETAIL,
arguments = listOf(navArgument("paymentHash") { type = NavType.StringType }) arguments = listOf(navArgument("checkingId") { type = NavType.StringType })
) { backStackEntry -> ) { backStackEntry ->
val paymentHash = backStackEntry.arguments?.getString("paymentHash") ?: "" val checkingId = backStackEntry.arguments?.getString("checkingId") ?: ""
val historyState by historyVm.state.collectAsState() // val historyState by historyVm.state.collectAsState()
val payment = (historyState as? HistoryState.Success) val historyState by historyVm.filteredState.collectAsState()
?.payments val payment = historyVm.findPayment(checkingId)
?.firstOrNull { it.paymentHash == paymentHash }
if (payment != null) { if (payment != null) {
PaymentDetailScreen( PaymentDetailScreen(
@@ -340,7 +339,7 @@ fun WalletScreen(
onBack = { navController.popBackStack() } onBack = { navController.popBackStack() }
) )
} else { } else {
LaunchedEffect(paymentHash) { historyVm.refresh() } LaunchedEffect(checkingId) { historyVm.refresh() }
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@@ -7,7 +7,6 @@ import com.bitcointxoko.gudariwallet.api.LnurlScanResponse
import com.bitcointxoko.gudariwallet.data.NodeAliasRepository import com.bitcointxoko.gudariwallet.data.NodeAliasRepository
import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
import com.bitcointxoko.gudariwallet.data.WalletRepository import com.bitcointxoko.gudariwallet.data.WalletRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -57,17 +57,10 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.bitcointxoko.gudariwallet.api.PaymentRecord import com.bitcointxoko.gudariwallet.api.PaymentRecord
import com.bitcointxoko.gudariwallet.ui.history.DatePreset
import com.bitcointxoko.gudariwallet.ui.history.DirectionFilter
import com.bitcointxoko.gudariwallet.ui.HistoryState import com.bitcointxoko.gudariwallet.ui.HistoryState
import com.bitcointxoko.gudariwallet.ui.history.HistoryViewModel
import com.bitcointxoko.gudariwallet.ui.history.PaymentFilter
import com.bitcointxoko.gudariwallet.ui.history.StatusFilter
import com.bitcointxoko.gudariwallet.ui.history.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
// ── List screen ────────────────────────────────────────────────────────────── // ── List screen ──────────────────────────────────────────────────────────────
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun HistoryScreen( fun HistoryScreen(
@@ -265,7 +258,6 @@ fun HistoryScreen(
} }
} }
} }
// ── Sheets (outside Scaffold so they overlay correctly) ────────────────── // ── Sheets (outside Scaffold so they overlay correctly) ──────────────────
if (filterSheetVisible) { if (filterSheetVisible) {
FilterBottomSheet( FilterBottomSheet(
@@ -292,7 +284,6 @@ fun HistoryScreen(
} }
// ── Search bar ─────────────────────────────────────────────────────────────── // ── Search bar ───────────────────────────────────────────────────────────────
@Composable @Composable
private fun SearchBar( private fun SearchBar(
query : String, query : String,
@@ -357,7 +348,6 @@ private fun SearchBar(
} }
// ── Active filter chips ────────────────────────────────────────────────────── // ── Active filter chips ──────────────────────────────────────────────────────
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun ActiveFilterChips( private fun ActiveFilterChips(
@@ -22,6 +22,8 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
private const val TAG = "HistoryViewModel"
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
class HistoryViewModel( class HistoryViewModel(
private val repo : WalletRepository, private val repo : WalletRepository,
@@ -30,7 +32,6 @@ class HistoryViewModel(
val fiatCurrency : StateFlow<String?>, val fiatCurrency : StateFlow<String?>,
val fiatSatsPerUnit : StateFlow<Double?> val fiatSatsPerUnit : StateFlow<Double?>
) : ViewModel() { ) : ViewModel() {
// ── Sync manager (owns _state, currentOffset, loadJob, sync loop) ───────── // ── Sync manager (owns _state, currentOffset, loadJob, sync loop) ─────────
private val syncManager: PaymentSyncManager = PaymentSyncManager( private val syncManager: PaymentSyncManager = PaymentSyncManager(
repo = repo, repo = repo,
@@ -38,7 +39,6 @@ class HistoryViewModel(
scope = viewModelScope, scope = viewModelScope,
onNewPayments = { payments -> enricher.enrich(payments) } onNewPayments = { payments -> enricher.enrich(payments) }
) )
private val enricher: PaymentEnricher = PaymentEnricher( private val enricher: PaymentEnricher = PaymentEnricher(
repo = repo, repo = repo,
aliasRepo = aliasRepo, aliasRepo = aliasRepo,
@@ -48,9 +48,7 @@ class HistoryViewModel(
syncManager.updateEnrichedPayment(paymentHash, pubkey, alias) syncManager.updateEnrichedPayment(paymentHash, pubkey, alias)
} }
) )
val state: StateFlow<HistoryState> = syncManager.state val state: StateFlow<HistoryState> = syncManager.state
fun refresh() = syncManager.refresh() fun refresh() = syncManager.refresh()
fun loadMore() = syncManager.loadMore( fun loadMore() = syncManager.loadMore(
filteredCanLoadMore = { (filteredState.value as? HistoryState.Success)?.canLoadMore == true } filteredCanLoadMore = { (filteredState.value as? HistoryState.Success)?.canLoadMore == true }
@@ -63,10 +61,8 @@ class HistoryViewModel(
scope = viewModelScope scope = viewModelScope
) )
val detailState: StateFlow<DetailState> = detailDelegate.state val detailState: StateFlow<DetailState> = detailDelegate.state
fun loadDetail(checkingId: String, record: PaymentRecord? = null) = fun loadDetail(checkingId: String, record: PaymentRecord? = null) =
detailDelegate.load(checkingId, record) detailDelegate.load(checkingId, record)
fun clearDetail() = detailDelegate.clear() fun clearDetail() = detailDelegate.clear()
// ── Filter state ────────────────────────────────────────────────────────── // ── Filter state ──────────────────────────────────────────────────────────
@@ -101,11 +97,18 @@ class HistoryViewModel(
val (min, max) = preset.toEpochSecondRange() // ← delegate to PaymentFilter.kt val (min, max) = preset.toEpochSecondRange() // ← delegate to PaymentFilter.kt
setDateFilter(min, max, preset) setDateFilter(min, max, preset)
} }
fun clearDateFilter() { fun clearDateFilter() {
_filter.update { it.copy(minCreatedAt = null, maxCreatedAt = null, datePreset = null) } _filter.update { it.copy(minCreatedAt = null, maxCreatedAt = null, datePreset = null) }
} }
fun findPayment(checkingId: String): PaymentRecord? {
val fromFiltered = (filteredState.value as? HistoryState.Success)
?.payments?.firstOrNull { it.checkingId == checkingId }
if (fromFiltered != null) return fromFiltered
return (state.value as? HistoryState.Success)
?.payments?.firstOrNull { it.checkingId == checkingId }
}
// ── Room-backed query results ────────────────────────────────────────────── // ── Room-backed query results ──────────────────────────────────────────────
private val _roomResults = MutableStateFlow<List<PaymentRecord>?>(null) private val _roomResults = MutableStateFlow<List<PaymentRecord>?>(null)
@@ -165,7 +168,6 @@ class HistoryViewModel(
} }
} }
} }
viewModelScope.launch { viewModelScope.launch {
WalletNotificationService.paymentEvents.collect { event -> WalletNotificationService.paymentEvents.collect { event ->
Log.d(TAG, "PAYMENTS [WS EVENT ] incoming event ${event.paymentHash} — refreshing list") Log.d(TAG, "PAYMENTS [WS EVENT ] incoming event ${event.paymentHash} — refreshing list")
@@ -173,10 +175,6 @@ class HistoryViewModel(
} }
} }
} }
companion object {
private const val TAG = "HistoryViewModel"
}
} }
// ── Factory ─────────────────────────────────────────────────────────────────── // ── Factory ───────────────────────────────────────────────────────────────────
@@ -198,4 +196,4 @@ class HistoryViewModelFactory(
fiatSatsPerUnit = fiatSatsPerUnit fiatSatsPerUnit = fiatSatsPerUnit
) as T ) as T
} }
} }
@@ -55,6 +55,5 @@ class PaymentDetailDelegate(
} }
} }
} }
fun clear() { _state.value = DetailState.Idle } fun clear() { _state.value = DetailState.Idle }
} }
@@ -33,9 +33,8 @@ internal fun PaymentRow(
fiatSatsPerUnit : Double?, fiatSatsPerUnit : Double?,
onClick : () -> Unit onClick : () -> Unit
) { ) {
val semantic = semanticColors()
val isPending = isPendingStatus(payment.status) val isPending = isPendingStatus(payment.status)
val semantic = semanticColors()
val amountColor = paymentAmountColor(payment.status, payment.isOutgoing) val amountColor = paymentAmountColor(payment.status, payment.isOutgoing)
val icon = if (payment.isOutgoing) Icons.Default.ArrowUpward val icon = if (payment.isOutgoing) Icons.Default.ArrowUpward
else Icons.Default.ArrowDownward else Icons.Default.ArrowDownward