diff options
| author | Adam Soltys <[email protected]> | 2026-10-22 07:32:50 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-22 07:32:50 -0700 |
| commit | 743011c02300ef884333d41c87a88900173023c9 (patch) | |
| tree | b5cab06fa9e8f5d4462281d96a37e64b1564f830 /tools | |
| parent | d0752d70b54e9d58bba5e11aea06c9ba1678e89b (diff) | |
| download | coinosv3-743011c02300ef884333d41c87a88900173023c9.tar.gz coinosv3-743011c02300ef884333d41c87a88900173023c9.zip | |
iPhone: every push ends in a notification, so iOS stops dropping the wallet's push subscription
WebKit allows no silent pushes: a push that shows no notification is a
strike (even with the app open), and after three it removes the
subscription, so a closed wallet stopped waking for NWC requests until it
was opened again (reported by PullCap). On an Apple endpoint, the paths
that used to end quietly (lookups answered, another device took the
request, DMs/reactions we don't announce, an open window, unknown pushes)
now show a silent self-replacing "Coinos" line, closed at once when the
app is on screen. A backgrounded iOS app is suspended, so an NWC request
is no longer handed to it: the worker answers itself.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/sw-push-quota.test.js | 79 |
1 files changed, 79 insertions, 0 deletions
diff --git a/tools/sw-push-quota.test.js b/tools/sw-push-quota.test.js new file mode 100644 index 0000000..e7d00f2 --- /dev/null +++ b/tools/sw-push-quota.test.js @@ -0,0 +1,79 @@ +// WebKit drops a push subscription after three pushes that show no +// notification, so on an Apple endpoint every path through the worker's push +// handler must end in one. Run: bun test tools/sw-push-quota.test.js +import { test, expect, mock } from 'bun:test'; + +let respond = async () => true; +mock.module('../src/nwc-respond.js', () => ({ respondFromBg: (...a) => respond(...a), tendRenewals: async () => {} })); +mock.module('../src/nwc-lock.js', () => ({ withRequestLock: async (_id, fn) => fn() })); +mock.module('../src/bg-autowithdraw.js', () => ({ bgAutoWithdraw: async () => {} })); +mock.module('../src/dm-inbox.js', () => ({ + allInboxes: async () => [{ muted: ['m'.repeat(64)] }], + classifyDm: async () => ({ own: true }), shouldNotifyDm: () => false, classifyCall: async () => null, +})); + +const listeners = {}; +let endpoint = 'https://web.push.apple.com/abc'; +let windows = []; +const shown = [], closed = []; +globalThis.self = { + location: { hostname: 'v3.coinos.io', origin: 'https://v3.coinos.io' }, + addEventListener: (t, f) => { listeners[t] = f; }, + clients: { matchAll: async () => windows }, + registration: { + pushManager: { getSubscription: async () => ({ endpoint }) }, + showNotification: async (title, o) => { shown.push({ title, ...o }); }, + getNotifications: async ({ tag }) => shown.filter((n) => n.tag === tag).map((n) => ({ close: () => closed.push(n) })), + }, +}; +await import('../src/sw-nwc.js'); + +async function push(data) { + shown.length = 0; closed.length = 0; + let done; + listeners.push({ data: { json: () => data }, waitUntil: (p) => { done = p; } }); + await done; + return shown.map((n) => n.title); +} +const nwc = { type: 'nwc', servicePubkey: 'a'.repeat(64), event: { id: 'e'.repeat(64), kind: 23194 } }; +const hidden = { visibilityState: 'hidden', postMessage: () => {} }; +const visible = { visibilityState: 'visible', postMessage: () => {} }; + +test('apple: every quiet path shows the stand-in', async () => { + endpoint = 'https://web.push.apple.com/abc'; windows = []; + respond = async () => true; // a lookup answered, or another device took it + expect(await push(nwc)).toEqual(['Coinos']); + expect(await push({ type: 'notify', reason: 'dm', wrap: {} })).toEqual(['Coinos']); + expect(await push({ type: 'notify', reason: 'reaction', from: 'm'.repeat(64) })).toEqual(['Coinos']); + expect(await push({ type: 'notify', reason: 'call', wrapId: 'x' })).toEqual(['Coinos']); + expect(await push({ type: 'weird' })).toEqual(['Coinos']); + expect(closed.length).toBe(0); +}); + +test('apple: an app on screen gets the stand-in closed at once', async () => { + windows = [visible]; + expect(await push({ type: 'notify', reason: 'payment', amountSat: 5 })).toEqual(['Coinos']); + expect(closed.length).toBe(1); +}); + +test('apple: a suspended background app does not swallow the request', async () => { + windows = [hidden]; + let asked = 0; + respond = async () => { asked++; return { notify: { title: 'Payment sent', body: '' } }; }; + expect(await push(nwc)).toEqual(['Payment sent']); + expect(asked).toBe(1); +}); + +test('elsewhere: quiet stays quiet', async () => { + endpoint = 'https://fcm.googleapis.com/fcm/send/abc'; windows = []; + respond = async () => true; + expect(await push(nwc)).toEqual([]); + windows = [hidden]; + let asked = 0; + respond = async () => { asked++; return true; }; + expect(await push(nwc)).toEqual([]); + expect(asked).toBe(0); // the open (if hidden) window answers on Android + windows = []; + respond = async () => false; + expect(await push(nwc)).toEqual(['Payment request']); +}); |
