diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/features/messages.js | 17 | ||||
| -rw-r--r-- | src/features/nwc.js | 44 | ||||
| -rw-r--r-- | src/i18n.js | 3 |
3 files changed, 55 insertions, 9 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index e1f3ea3..b12efa7 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -1278,6 +1278,10 @@ export function messagesFeature(ctx) { const { publicKey } = await (await fetch(`${NOTIFIER}/vapid`)).json(); const wantKey = b64ToBytes(publicKey); let sub = await reg.pushManager.getSubscription(); + // see nwc.js: a Chromium without Google Play Services (GrapheneOS, and + // any de-Googled Android) has no push service at all, and says so in a + // way nobody can act on. Remember it so the offer can explain itself. + const noPushService = (e) => /push service|Registration failed|AbortError|NotSupportedError/i.test((e && (e.message || e.name)) || ''); // A subscription made against a DIFFERENT VAPID key can never receive a // push — the notifier's sends fail with a permanent 403 and it keeps // reusing the dead sub, so the device goes silent forever (this is what @@ -1291,8 +1295,14 @@ export function messagesFeature(ctx) { }; if (sub && !keyMatches(sub)) { try { await sub.unsubscribe(); } catch {} sub = null; } if (!sub) { - sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: wantKey }); + try { + sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: wantKey }); + } catch (e) { + if (noPushService(e)) { const s2 = st(); s2.noPushService = true; save(s2); render(); } + return false; + } } + { const s2 = st(); if (s2.noPushService) { delete s2.noPushService; save(s2); } } const r = await fetch(`${NOTIFIER}/register`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ subscription: sub.toJSON(), notify: pushWatch() }), @@ -3969,6 +3979,11 @@ export function messagesFeature(ctx) { class: 'btn-ghost btn-sm', onClick: () => { const s = st(); s.pushDismissed = true; save(s); render(); }, }, t('msgDismiss'))))); + // A browser with no push service at all: the offer above is beside the + // point (permission may even be granted), so say what's actually wrong + // wherever the user got to. + if (st().noPushService && !st().push) + kids.push(h('div', { class: 'notice info small' }, t('nwcNoPushService'))); // ---- the feed, above the conversations: it's the thing you read, they're // the things you answer diff --git a/src/features/nwc.js b/src/features/nwc.js index 45bbbf5..077b53e 100644 --- a/src/features/nwc.js +++ b/src/features/nwc.js @@ -584,18 +584,39 @@ export function nwcFeature(ctx) { return Uint8Array.from(raw, (c) => c.charCodeAt(0)); }; + // Waking a closed app needs a push service, and on Android that means the + // browser's: Chrome (and every Chromium, including GrapheneOS's Vanadium) + // routes web push through Firebase, which isn't there on a phone without + // Google Play Services. The browser says so as "Registration failed - push + // service error", which tells the user nothing. Translate it, and name the + // thing that does work. + const isNoPushService = (e) => { + const m = (e && (e.message || e.name)) || ''; + return /push service|Registration failed|AbortError|NotSupportedError|permission denied/i.test(m); + }; + async function enableBackground() { if (!('serviceWorker' in navigator) || !('PushManager' in window)) { - throw new Error('this browser cannot receive push'); + throw new Error(t('nwcNoPushHere')); } const perm = await Notification.requestPermission(); - if (perm !== 'granted') throw new Error('notifications were not allowed'); + if (perm !== 'granted') throw new Error(t('nwcNoPermission')); const reg = await navigator.serviceWorker.ready; const { publicKey } = await (await fetch(`${NOTIFIER}/vapid`)).json(); - const sub = await reg.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: b64ToBytes(publicKey), - }); + let sub; + try { + sub = await reg.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: b64ToBytes(publicKey), + }); + } catch (e) { + if (isNoPushService(e)) { + const st2 = load(); st2.pushUnavailable = true; save(st2); + throw new Error(t('nwcNoPushService')); + } + throw e; + } + { const st2 = load(); if (st2.pushUnavailable) { delete st2.pushUnavailable; save(st2); } } const pks1 = conns().map((c) => c.servicePk); if (load().offer) pks1.push(load().offer.pk); // Arming can precede the first connection (creation requires it) — the @@ -807,7 +828,11 @@ export function nwcFeature(ctx) { : h('button', { class: 'btn-sm', onClick: () => { ui.nwcShow = c.id; render(); } }, t('nwcShow'))); }), list.length - ? h('div', { class: 'row between', style: 'border-top:1px solid var(--border,rgba(128,128,128,.2));padding-top:8px' }, + ? h('div', { class: 'col', style: 'gap:6px;border-top:1px solid var(--border,rgba(128,128,128,.2));padding-top:8px' }, + load().pushUnavailable && !load().background + ? h('div', { class: 'small faint' }, t('nwcNoPushService')) + : null, + h('div', { class: 'row between' }, h('span', { class: 'small' }, t('nwcBackground')), h('button', { class: 'btn-sm', onClick: async () => { try { @@ -817,6 +842,9 @@ export function nwcFeature(ctx) { const s = load(); s.backgroundOff = true; save(s); toast(t('nwcBackgroundOff')); } else { + // an explicit tap always retries: a phone that grew a push + // service (or a different browser) deserves another go + const s0 = load(); delete s0.pushUnavailable; save(s0); await enableBackground(); const s = load(); delete s.backgroundOff; save(s); await reconcileBg(); @@ -824,7 +852,7 @@ export function nwcFeature(ctx) { } } catch (e) { toast(e.message); } render(); - } }, load().background ? t('nwcOn') : t('nwcOff'))) + } }, load().background ? t('nwcOn') : t('nwcOff')))) : null, st ? h('div', { class: 'col', style: 'gap:6px;border-top:1px solid var(--border,rgba(128,128,128,.2));padding-top:8px' }, diff --git a/src/i18n.js b/src/i18n.js index 4121df1..38836d2 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -852,6 +852,9 @@ const STR = { nwcTabWarning: 'Works while Coinos is open — turn on background answering below to keep working when it isn’t.', nwcAdd: 'Add a connection', nwcBackground: 'Answer while closed', + nwcNoPushHere: 'This browser can\u2019t receive push messages.', + nwcNoPermission: 'Notifications weren\u2019t allowed.', + nwcNoPushService: 'This browser has no push service, so it can\u2019t be woken while closed. On Android that\u2019s usually a phone without Google Play Services \u2014 Firefox brings its own push and works there. Zaps are still answered whenever coinos is open.', nwcOn: 'On', nwcOff: 'Off', nwcBackgroundOn: 'Requests will be answered in the background', |
