aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-22 10:32:22 -0700
committerAdam Soltys <[email protected]>2026-10-22 10:32:22 -0700
commit1ea1784c4ad7d6715c0efdcd0078019c692eefe5 (patch)
treee5f86cd28d103341114930444c3980cda72257ac /tools
parentd59a7f0c9b32002ff23f74a2996b986cf1f0dd05 (diff)
downloadcoinosv3-1ea1784c4ad7d6715c0efdcd0078019c692eefe5.tar.gz
coinosv3-1ea1784c4ad7d6715c0efdcd0078019c692eefe5.zip
iPhone: a push subscription iOS dropped is made again when the app opens or comes back
WebKit removes a subscription after three pushes that showed no notification (743011c stops new strikes, but a wallet already struck out stayed deaf): background stayed on, getSubscription() came back empty and refreshRegistration gave up, so NWC requests stopped waking the closed app until the switch was toggled (reported by PullCap). It now subscribes again (also when the VAPID key changed) and re-registers with the notifier, on start and whenever the app returns to the screen. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01K5xtiFrkFVsrkWx6JDQjiT
Diffstat (limited to 'tools')
-rw-r--r--tools/push-resubscribe-test.js103
1 files changed, 103 insertions, 0 deletions
diff --git a/tools/push-resubscribe-test.js b/tools/push-resubscribe-test.js
new file mode 100644
index 0000000..19998b2
--- /dev/null
+++ b/tools/push-resubscribe-test.js
@@ -0,0 +1,103 @@
+// iOS drops a web push subscription after three pushes that showed no
+// notification, and nothing used to make a new one: the wallet's background
+// switch stayed on, getSubscription() came back empty, and the notifier kept
+// a dead endpoint, so NWC requests stopped waking the closed app until the
+// user toggled it off and on (reported by PullCap). Opening the app, or
+// coming back to it, must subscribe again and tell the notifier.
+//
+// Run: bun tools/push-resubscribe-test.js
+import puppeteer from './puppeteer.js';
+import { generateMnemonic } from '@scure/bip39';
+import { wordlist } from '@scure/bip39/wordlists/english';
+import { cacheKeyFor } from '../src/wallet.js';
+import { buildHtml } from '../build.js';
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+let ok = true;
+const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; };
+const html = await buildHtml({ minify: true, pwa: false });
+const server = Bun.serve({ port: 5263, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
+const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const page = await browser.newPage();
+const KEY = 'BP' + 'A'.repeat(85); // any 65-byte key, base64url
+await page.evaluateOnNewDocument((KEY) => {
+ const bytes = () => Uint8Array.from(atob(KEY.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0));
+ window.__subs = 0; window.__regs = [];
+ let sub = null; // iOS has already dropped it
+ const make = () => ({
+ endpoint: 'https://web.push.apple.com/sub' + window.__subs,
+ options: { applicationServerKey: bytes().buffer },
+ toJSON() { return { endpoint: this.endpoint, keys: { p256dh: 'x', auth: 'y' } }; },
+ unsubscribe: () => { sub = null; return Promise.resolve(true); },
+ });
+ window.__drop = () => { sub = null; };
+ const reg = {
+ pushManager: {
+ getSubscription: () => Promise.resolve(sub),
+ subscribe: () => { window.__subs++; sub = make(); return Promise.resolve(sub); },
+ },
+ showNotification: () => Promise.resolve(),
+ };
+ Object.defineProperty(navigator.serviceWorker, 'ready', { get: () => Promise.resolve(reg) });
+ Notification.requestPermission = () => Promise.resolve('granted');
+ Object.defineProperty(Notification, 'permission', { get: () => 'granted' });
+ const f = window.fetch;
+ window.fetch = async (u, o) => {
+ const url = String(u);
+ if (url.startsWith('https://nwcpush.coinos.io/vapid')) return new Response(JSON.stringify({ publicKey: KEY }));
+ if (url.startsWith('https://nwcpush.coinos.io/register')) {
+ const b = JSON.parse(o.body);
+ if (b.servicePubkeys) window.__regs.push(b.subscription.endpoint);
+ return new Response('{"ok":true}');
+ }
+ return f(u, o);
+ };
+}, KEY);
+const click = (t) => page.evaluate((x) => { const e = [...document.querySelectorAll('button')].find((n) => n.textContent.trim().toLowerCase().includes(x)); if (e) { e.click(); return true; } return false; }, t);
+const waitText = async (x, ms = 25000) => { for (let i = 0; i < ms/250; i++) { if ((await page.evaluate(() => document.body.innerText)).toLowerCase().includes(x)) return true; await sleep(250); } return false; };
+try {
+ await page.setViewport({ width: 390, height: 844 });
+ await page.goto('http://localhost:5263/', { waitUntil: 'domcontentloaded' });
+ await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
+ await page.reload({ waitUntil: 'domcontentloaded' });
+ await sleep(400);
+ await click('create a new wallet'); await sleep(500);
+ await click('to import'); await sleep(400);
+ await page.waitForSelector('textarea');
+ const mn = generateMnemonic(wordlist);
+ await page.type('textarea', mn);
+ await click('open wallet');
+ await waitText('receive', 20000);
+ const base = cacheKeyFor(mn + '\n');
+
+ // a wallet answering in the background, whose subscription iOS took away
+ await page.evaluate((k) => localStorage.setItem(k + ':nwc', JSON.stringify({
+ conns: [{ id: 'x', name: 'test', servicePk: 'a'.repeat(64), sk: 'b'.repeat(64), maxSat: 1000, budget: {} }],
+ background: true,
+ })), base);
+ await page.reload({ waitUntil: 'domcontentloaded' });
+ await waitText('receive', 20000);
+ await sleep(3000);
+ let s = await page.evaluate(() => ({ subs: window.__subs, regs: window.__regs }));
+ check('opening the app subscribes again', s.subs >= 1, `${s.subs} subscribe()`);
+ check('...and registers the new endpoint for the connection', s.regs.includes('https://web.push.apple.com/sub' + s.subs), s.regs.join(' '));
+
+ // dropped again while the app sat in the background; it resumes, no reload
+ await page.evaluate(() => { window.__drop(); window.__regs.length = 0; });
+ await page.evaluate(() => {
+ Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => 'visible' });
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+ await sleep(3000);
+ const before = s.subs;
+ s = await page.evaluate(() => ({ subs: window.__subs, regs: window.__regs }));
+ check('coming back to the app subscribes again', s.subs > before, `${s.subs} subscribe()`);
+ check('...and registers it', s.regs.includes('https://web.push.apple.com/sub' + s.subs), s.regs.join(' '));
+
+ // a live subscription is left alone
+ await page.evaluate(() => { window.__regs.length = 0; document.dispatchEvent(new Event('visibilitychange')); });
+ await sleep(3000);
+ const again = await page.evaluate(() => ({ subs: window.__subs, regs: window.__regs }));
+ check('a subscription still there is reused', again.subs === s.subs && again.regs.length >= 1);
+} finally { await browser.close(); server.stop(true); }
+console.log(ok ? '\n✅ a dropped push subscription comes back' : '\n❌ failed');
+process.exit(ok ? 0 : 1);