fix(wallet): a seen receipt stays seen across refreshes
Demo images / Build & push demo images (push) Failing after 2m6s
Demo images / Build & push demo images (push) Failing after 2m6s
fc98c1d8 replaced the five-minute timer with "stays until seen", but
kept "seen" in component state — so every page load forgot it and the
entire ecash history came back as new. That is worse than the timer it
replaced: the old behaviour at least let receipts go, this one resurrected
them on every refresh. Reported from the node, and correctly.
Acknowledgement now lives in localStorage, capped at 300 keys.
That opens the opposite trap: on a browser with nothing stored, treating
the whole history as unseen is the same wall of old receipts from the
other direction. So a first run seeds everything older than five minutes
as already seen — the window survives as a first-run heuristic, not as
an expiry. Unreadable storage takes the same path, because reading a
corrupt value as "nothing acknowledged" is the refresh bug wearing a hat.
Also guards the balance readout against NaN. `sats == null` does not
catch it, and arithmetic over a missing field produces it, so it would
have rendered as the literal text "NaN sats" — worse than the zero the
component exists to prevent, since a zero at least looks like a number.
Frontend: 1000 tests green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
603291008b
commit
59440ef1ac
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import i18n from '@/i18n'
|
||||
import HomeWalletCard from '@/views/home/HomeWalletCard.vue'
|
||||
@@ -49,10 +49,16 @@ const mountCard = (transactions: ReturnType<typeof tx>[]) =>
|
||||
global: { plugins: [i18n] },
|
||||
})
|
||||
|
||||
// Acknowledgement is stored per-browser, so each case starts from a clean
|
||||
// slate — otherwise one test's "seen" set silently satisfies the next.
|
||||
beforeEach(() => localStorage.clear())
|
||||
|
||||
describe('incoming payments', () => {
|
||||
it('keeps an hours-old instant receipt in the badge until it is seen', async () => {
|
||||
// The regression: two hours past the old five-minute window. Nobody has
|
||||
// looked at it yet, so it is still news.
|
||||
it('retires a receipt on acknowledgement, never on age alone', async () => {
|
||||
// On a browser that has acknowledged before, an unacknowledged receipt
|
||||
// stays put however old it is — age is not the signal, being seen is.
|
||||
// (A brand-new browser is the separate first-run case below.)
|
||||
localStorage.setItem('archy-seen-incoming-v1', JSON.stringify(['some-earlier-receipt']))
|
||||
const w = mountCard([tx({ time_stamp: now() - 7200 })])
|
||||
expect(w.text()).toContain('Incoming 1')
|
||||
})
|
||||
@@ -67,6 +73,7 @@ describe('incoming payments', () => {
|
||||
})
|
||||
|
||||
it('marks it seen once the panel is closed again', async () => {
|
||||
localStorage.setItem('archy-seen-incoming-v1', JSON.stringify([]))
|
||||
const w = mountCard([tx()])
|
||||
const badge = w.find('button')
|
||||
await badge.trigger('click') // open
|
||||
@@ -108,6 +115,46 @@ describe('incoming payments', () => {
|
||||
expect(w.text()).toContain('Incoming 3')
|
||||
})
|
||||
|
||||
it('does not show a receipt again after a refresh', async () => {
|
||||
// The bug this whole model was supposed to prevent, and briefly caused:
|
||||
// "seen" lived in component state, so every page load forgot it and the
|
||||
// entire ecash history came back as new. Remounting is a refresh.
|
||||
const received = tx({ amount_sats: 4200, time_stamp: now() - 30 })
|
||||
const first = mountCard([received])
|
||||
const badge = first.find('button')
|
||||
await badge.trigger('click')
|
||||
await badge.trigger('click')
|
||||
expect(first.text()).not.toContain('Incoming 1')
|
||||
|
||||
const afterRefresh = mountCard([received])
|
||||
expect(afterRefresh.text()).not.toContain('Incoming 1')
|
||||
})
|
||||
|
||||
it('does not greet a brand-new browser with the whole history', () => {
|
||||
// Nothing acknowledged yet and a long history: treating all of it as
|
||||
// unseen would be the same wall of old receipts, just from the other
|
||||
// direction. Only what is genuinely recent counts as news on a first run.
|
||||
const old = [
|
||||
tx({ amount_sats: 100, time_stamp: now() - 86400 }),
|
||||
tx({ amount_sats: 200, time_stamp: now() - 3600 }),
|
||||
tx({ amount_sats: 300, time_stamp: now() - 600 }),
|
||||
]
|
||||
expect(mountCard(old).text()).not.toContain('Incoming')
|
||||
|
||||
// …but a receipt from moments ago still is.
|
||||
localStorage.clear()
|
||||
expect(mountCard([...old, tx({ amount_sats: 400, time_stamp: now() - 5 })]).text())
|
||||
.toContain('Incoming 1')
|
||||
})
|
||||
|
||||
it('survives unreadable storage without resurrecting the history', () => {
|
||||
// A corrupt value must not read as "nothing has been acknowledged" — that
|
||||
// is precisely the refresh bug wearing a different hat.
|
||||
localStorage.setItem('archy-seen-incoming-v1', '{not json')
|
||||
const w = mountCard([tx({ amount_sats: 100, time_stamp: now() - 86400 })])
|
||||
expect(w.text()).not.toContain('Incoming')
|
||||
})
|
||||
|
||||
it('does not silently turn into a navigation button while the panel is open', async () => {
|
||||
// The badge is two controls in one: with receipts it toggles the panel,
|
||||
// without them it navigates to the full transactions view. If the list
|
||||
|
||||
Reference in New Issue
Block a user