aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-22 07:32:50 -0700
committerAdam Soltys <[email protected]>2026-10-22 07:32:50 -0700
commit743011c02300ef884333d41c87a88900173023c9 (patch)
treeb5cab06fa9e8f5d4462281d96a37e64b1564f830 /tools
parentd0752d70b54e9d58bba5e11aea06c9ba1678e89b (diff)
downloadcoinosv3-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.js79
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']);
+});