walletCatchUpCueProvider top-level property
See WalletCatchUpCue. A plain derived Provider: every input is already
live state, so this re-evaluates exactly when they change.
Implementation
final walletCatchUpCueProvider = Provider<WalletCatchUpCue>((ref) {
final rescan = ref.watch(walletRescanControllerProvider);
if (rescan is WalletRescanRebuilding) {
return WalletCatchUpRebuilding(target: rescan.target);
}
final session = ref.watch(walletSessionProvider);
if (session == null) return const WalletCatchUpNone();
// Last-known snapshot (`.value` — same retention idiom as the screen): a
// transient re-read failure must not flap the cue. No snapshot yet ⇒ no
// verdict ⇒ no cue (the screen is still on its cold-load spinner anyway).
final snapshot = ref.watch(walletSnapshotProvider).value;
if (snapshot == null || snapshot.lastSynced != null) {
return const WalletCatchUpNone();
}
// The cold snapshot's own status carries the first frame before the live
// stream emits (the screen's `liveStatus ?? state.syncStatus` idiom).
final status = ref.watch(syncStatusProvider).value ?? snapshot.syncStatus;
if (status is SyncStatus_UpToDate) return const WalletCatchUpNone();
// #357 (post-ship reliability/UX HIGH fix): the durable everSynced flag is now
// CLEARED on a rescan by the core (its aux row is reset like `sync_stamp`), so
// `everSynced == true` UNAMBIGUOUSLY means "reached tip since the last rescan ⇒
// the balance is complete" — suppress the over-explanation of a settled balance
// behind a stale-null stamp (a swallowed stamp-write fault / pre-#317 wallet /
// offline relaunch), with NO scanning-or-balance heuristic. A rescan REBUILD has
// everSynced CLEARED, so it falls through and correctly shows the cue for its
// whole catch-up — INCLUDING offline after a process death (LMK kill), the panic
// window an earlier `status is Scanning` proxy wrongly suppressed. A never-synced
// wallet (`everSynced == false`) likewise falls through to the first-run cue.
if (snapshot.everSynced) return const WalletCatchUpNone();
// A tip this SESSION already proved stays proved: the
// stamp's visibility here rides an async snapshot re-read that can fault
// (busy DB, `.value` retention) or simply not be wired (a host mounting
// only the swap surface) — without this, a routine post-tip scan or an
// offline drop would resurrect "catching up" over a FINAL balance. The
// latch is session-keyed (resets on the rescan's own swap) and held
// through routine re-scan windows by design, which is exactly the
// no-flap semantic the cue needs. An INVALIDATED latch (deep-reorg
// rewind) deliberately falls through: over a never-stamped wallet a
// rewound re-scan genuinely is a catch-up.
if (ref.watch(walletSyncedTipProvider) is WalletSyncedTipLatched) {
return const WalletCatchUpNone();
}
// #377 s357b-2: the durable rescan-rebuilding breadcrumb — a rescan swapped
// in and has not reached tip since (the core clears it at the first
// post-rescan clean pass). The in-session fast path above already handled a
// LIVE rescan with its named target; reaching here with the breadcrumb set
// means the choice was lost (process death mid-rebuild — the LMK-kill
// window), so name the rescan generically rather than falling through to
// the first-run framing. Checked AFTER everSynced and the session latch:
// both prove the rebuild is over, so a stale breadcrumb (a swallowed
// clear fault at tip) can never pin this banner over a settled balance.
if (snapshot.rescanRebuilding) {
return const WalletCatchUpRebuilding(target: null);
}
return const WalletCatchUpSyncing();
});