aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-13 23:24:09 -0700
committerAdam Soltys <[email protected]>2026-08-13 23:24:09 -0700
commitb0980c2df8835965a0dc779f118e59610091134d (patch)
tree678daf74fd9d861a375d7bbe2ab0a192356f04ff /src/features
parent3104c93cece2104457d672aef59da2327659de4b (diff)
downloadcoinosv3-b0980c2df8835965a0dc779f118e59610091134d.tar.gz
coinosv3-b0980c2df8835965a0dc779f118e59610091134d.zip
No notifications, no connection: creating one asks to allow them first, and a denial explains itself instead of leaving a dead connection spinning in the other app
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_014tE4U5nEjGTQy8PwYqamfv
Diffstat (limited to 'src/features')
-rw-r--r--src/features/nwc.js45
1 files changed, 39 insertions, 6 deletions
diff --git a/src/features/nwc.js b/src/features/nwc.js
index 730299e..686b11f 100644
--- a/src/features/nwc.js
+++ b/src/features/nwc.js
@@ -571,11 +571,16 @@ export function nwcFeature(ctx) {
});
const pks1 = conns().map((c) => c.servicePk);
if (load().offer) pks1.push(load().offer.pk);
- const r = await fetch(`${NOTIFIER}/register`, {
- method: 'POST', headers: { 'content-type': 'application/json' },
- body: JSON.stringify({ subscription: sub.toJSON(), servicePubkeys: pks1 }),
- });
- if (!r.ok) throw new Error(`notifier refused: ${r.status}`);
+ // Arming can precede the first connection (creation requires it) — the
+ // notifier rejects an empty watch list, so defer that POST to the
+ // refreshRegistration() the creation triggers moments later.
+ if (pks1.length) {
+ const r = await fetch(`${NOTIFIER}/register`, {
+ method: 'POST', headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({ subscription: sub.toJSON(), servicePubkeys: pks1 }),
+ });
+ if (!r.ok) throw new Error(`notifier refused: ${r.status}`);
+ }
const st = load(); st.background = true; save(st);
writeBg();
return true;
@@ -602,6 +607,27 @@ export function nwcFeature(ctx) {
} catch (e) { console.log('nwc: background answering not armed:', e.message); }
}
+ // A connection that only answers while a tab happens to be open is a broken
+ // promise — creating one REQUIRES the background path. Throws a
+ // user-readable reason when it can't be armed; the caller shows it and
+ // does not create the connection.
+ async function requireBackground() {
+ if (load().background) return;
+ if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)
+ || typeof window === 'undefined' || !('PushManager' in window)
+ || typeof Notification === 'undefined') throw new Error(t('nwcNoPush'));
+ if (Notification.permission === 'denied') throw new Error(t('nwcNotifBlocked'));
+ try {
+ await enableBackground();
+ } catch (e) {
+ // Denied at the prompt (or dismissed it) — anything else is a real
+ // failure whose message is more useful than a permissions lecture.
+ if (Notification.permission !== 'granted') throw new Error(t('nwcNotifNeeded'));
+ throw e;
+ }
+ const s = load(); delete s.backgroundOff; save(s);
+ }
+
// Keep the notifier's view of our service pubkeys current. It can only
// wake this device for connections it was told about, and a registration
// made before a connection existed (or after one was revoked) watches the
@@ -779,9 +805,16 @@ export function nwcFeature(ctx) {
h('label', { class: 'field' }, h('span', { class: 'lab' }, t('nwcDailyBudget')),
h('input', { type: 'number', min: '1', value: st.dailySat,
onInput: (e) => { st.dailySat = e.target.value; } })),
+ (typeof Notification === 'undefined' || Notification.permission !== 'granted')
+ ? h('div', { class: 'small faint' }, t('nwcNotifHint'))
+ : null,
h('div', { class: 'row gap6' },
h('button', { class: 'btn-ghost', onClick: () => { ui.nwcNew = null; render(); } }, t('cancel')),
- h('button', { class: 'btn-primary grow', onClick: () => {
+ h('button', { class: 'btn-primary grow', onClick: async () => {
+ // No notifications, no connection: it would sit dead the
+ // moment the tab closes, spinning in the other app.
+ try { await requireBackground(); }
+ catch (e) { toast(e.message); return; }
const c = createConn({
name: st.name, maxSat: parseInt(st.maxSat, 10) || 10000,
dailySat: parseInt(st.dailySat, 10) || 50000,