diff options
| author | Adam Soltys <[email protected]> | 2026-09-12 10:32:01 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-12 10:32:01 -0700 |
| commit | 60e755286302647d93bc82070c61fcf93ddbc3b9 (patch) | |
| tree | 5cbe3c780010e975d61193138a29870f295427af /src/features | |
| parent | a846211262f37238c001e5210938749294f8db6a (diff) | |
| download | coinosv3-60e755286302647d93bc82070c61fcf93ddbc3b9.tar.gz coinosv3-60e755286302647d93bc82070c61fcf93ddbc3b9.zip | |
A second Android build that can wake the wallet, because it holds it.
The TWA can't. Chrome renders the site, so the wallet's keys live in Chrome's
storage for its own origin, and nothing outside Chrome can reach them — a
UnifiedPush message could raise a notification and no more. On a phone with
Google Play Services none of this matters; without it, background answering
was simply unavailable.
So: a second flavour, io.coinos.webview, "Coinos (UnifiedPush)". The wallet
runs in a WebView this app owns, which means a push can start it off-screen
and let it answer for itself. WakeService loads it with ?wake=<payload> into
the same storage the visible app uses, the page subscribes and answers
whatever the payload was about, then calls CoinosHost.done() so the service
stops — capped at 25 seconds from the page and 30 from the service, because
nothing here is allowed to sit in the background costing battery. Its
foreground notification says what it's doing rather than apologising for
existing.
The WebView carries what the wallet actually needs: DOM storage and
IndexedDB, the camera for the QR scanner (granted only once Android has
granted it to us), a file chooser for attaching pictures, and links to
anywhere but coinos.io handed to a real browser.
The trade is named in the code and worth naming here: a separate app is a
separate wallet. Its storage is its own, so signing in there is signing in
again. That's why this is a second build rather than a replacement — the TWA
is untouched, still the better browser for anyone whose phone has push.
The shared push receiver now asks the system for whatever front door this
build has, and asks a per-flavour Wake class whether the wallet can be woken
at all — no flavour checks in the middle of it. Both APKs build and ship:
coinos.apk as before, coinos-unifiedpush.apk beside it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
Diffstat (limited to 'src/features')
| -rw-r--r-- | src/features/nwc.js | 31 |
1 files changed, 30 insertions, 1 deletions
diff --git a/src/features/nwc.js b/src/features/nwc.js index dd0141d..cefeb31 100644 --- a/src/features/nwc.js +++ b/src/features/nwc.js @@ -242,6 +242,7 @@ export function nwcFeature(ctx) { content, }, sk); await publish(NWC_RELAYS, evt); + noteAnswered(); // a woken background run can stop once this happens } catch (e) { console.warn('nwc: could not reply', e.message); } @@ -794,6 +795,34 @@ export function nwcFeature(ctx) { }); } + // ---- woken by the Android wrapper -------------------------------------- + // The WebView build starts the wallet off-screen when a push arrives, with + // ?wake=<payload> on the URL. There's no service worker in that picture — + // the page IS the background — so it does the normal thing (subscribe, let + // the request arrive, answer it) and then says it's finished, so the + // service can stop rather than sit there costing battery. A cap, because + // "finished" is not always reachable: the relay may have nothing for us. + function serveWake() { + let payload = null; + try { payload = new URLSearchParams(location.search).get('wake'); } catch {} + if (payload == null) return; + try { history.replaceState(null, '', location.pathname || '/'); } catch {} + const host = typeof window !== 'undefined' && window.CoinosHost; + listen(); // the request is on a relay; this is what hears it + reconcileBg(); // and make sure the pouch behind it is current + if (!host || typeof host.done !== 'function') return; + let settled = false; + const finish = () => { if (settled) return; settled = true; try { host.done(); } catch {} }; + // an answer published is the signal we were woken for + const stop = onAnswered(() => setTimeout(finish, 1500)); + setTimeout(() => { try { stop(); } catch {} finish(); }, 25_000); + } + + // Callers that want to know when this wallet answered something. + const answeredWatchers = new Set(); + function onAnswered(fn) { answeredWatchers.add(fn); return () => answeredWatchers.delete(fn); } + function noteAnswered() { for (const fn of [...answeredWatchers]) { try { fn(); } catch {} } } + // Watchdog: relay sockets can die in ways the pool's reconnect never heals // (an errored socket sets skipReconnection and gives up), and a wallet // service with a dead subscription is indistinguishable from one that's @@ -913,7 +942,7 @@ export function nwcFeature(ctx) { id: 'nwc', nwcOfferPubkey() { return (hook('arkReady') && !wallet.watchOnly) ? offerKeys().pk : null; }, nwcOfferString() { return hook('arkReady') && !wallet.watchOnly ? offerString() : null; }, - init() { listen(); startWatchdog(); refreshRegistration(); reconcileBg(); ensureBackground(false); }, + init() { listen(); startWatchdog(); refreshRegistration(); reconcileBg(); ensureBackground(false); serveWake(); }, stop() { stop(); if (watchdog) { clearInterval(watchdog); watchdog = null; } }, nostrSettingsCards() { return [nwcCard()]; }, notifySettingsCards() { |
