From 5b23470f5d9ec7ee7cc33c2b4b936ddd24fa17cc Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 14 Aug 2026 01:13:47 -0700 Subject: Money leaving while you're not looking gets a notification: NWC pays from the worker or a hidden tab announce themselves — amount, fee, and which app asked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_014tE4U5nEjGTQy8PwYqamfv --- src/features/nwc.js | 17 +++++++++++++++++ src/i18n.js | 2 ++ src/nwc-respond.js | 11 ++++++++++- src/sw-nwc.js | 2 +- 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/features/nwc.js b/src/features/nwc.js index 686b11f..d6bfdea 100644 --- a/src/features/nwc.js +++ b/src/features/nwc.js @@ -212,6 +212,22 @@ export function nwcFeature(ctx) { ? nip44.encrypt(txt, nip44.getConversationKey(sk, pk)) : nip04.encrypt(sk, pk, txt)); + // A pay answered by a hidden tab is as invisible as one paid by the + // worker — surface it the same way. A visible tab already shows it live. + async function notifySent(c, amountSat, feeSat) { + try { + if (typeof document === 'undefined' || !document.hidden) return; + if (typeof Notification === 'undefined' || Notification.permission !== 'granted') return; + const reg = await navigator.serviceWorker.ready; + await reg.showNotification(t('nwcPaidTitle'), { + body: t('nwcPaidBody', { amount: amountSat.toLocaleString(), name: c.name || 'app' }) + + (feeSat ? ` (+${feeSat} fee)` : ''), + icon: 'icon-192.png', badge: 'icon-192.png', + tag: 'nwc-sent', renotify: true, data: { url: './' }, + }); + } catch {} + } + // Every request gets a reply, including failures — a client that never // hears back just hangs. async function reply(c, ev, scheme, payload) { @@ -358,6 +374,7 @@ export function nwcFeature(ctx) { const res = await hook('arkPayInvoice', invoice, { maxAmountSat: c.maxSat }); // the ark seam throws on failure, so reaching here means it settled recordSpend(c, (res.amountSat || 0) + (res.feeSat || 0)); + notifySent(c, res.amountSat || 0, res.feeSat || 0); return { result: { preimage: res.preimage, fees_paid: (res.feeSat || 0) * 1000 } }; } diff --git a/src/i18n.js b/src/i18n.js index 85f8308..a211eb5 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -663,6 +663,8 @@ const STR = { nwcOff: 'Off', nwcBackgroundOn: 'Requests will be answered in the background', nwcBackgroundOff: 'Background answering turned off', + nwcPaidTitle: 'Payment sent', + nwcPaidBody: '-{amount} sats via {name}', nwcNotifHint: 'You’ll be asked to allow notifications — the connection uses them to answer when Coinos is closed.', nwcNotifNeeded: 'Without notifications the connection couldn’t answer when Coinos is closed, so it wasn’t added. Allow notifications and try again.', nwcNotifBlocked: 'Notifications are blocked for this site. Allow them in your browser settings, then add the connection again.', diff --git a/src/nwc-respond.js b/src/nwc-respond.js index 258b7e7..bd75360 100644 --- a/src/nwc-respond.js +++ b/src/nwc-respond.js @@ -280,5 +280,14 @@ export async function respondFromBg(data, { result: { preimage: a.preimage, fees_paid: (a.feeSat || 0) * 1000 }, }); log(`paid ${a.amountSat || dec.amountSat} sat via the ASP from the background mirror`); - return true; + // Money left while the app was closed — that always deserves a heads-up, + // whether it was a zap from a nostr app or any other connected spender. + const paidSat = a.amountSat || dec.amountSat; + const feeNote = a.feeSat ? ` (+${a.feeSat} fee)` : ''; + return { + notify: { + title: 'Payment sent', tag: 'nwc-sent', + body: `-${paidSat.toLocaleString()} sats${feeNote} via ${conn.name || 'a connected app'}`, + }, + }; } diff --git a/src/sw-nwc.js b/src/sw-nwc.js index a73a611..108c393 100644 --- a/src/sw-nwc.js +++ b/src/sw-nwc.js @@ -80,7 +80,7 @@ self.addEventListener('push', (e) => { await self.registration.showNotification(handled.notify.title, { body: handled.notify.body, icon: 'icon-192.png', badge: 'icon-192.png', - tag: 'nwc-incoming', renotify: true, + tag: handled.notify.tag || 'nwc-incoming', renotify: true, data: { url: './' }, }); return; -- cgit v1.2.3