diff options
| -rw-r--r-- | android/app/src/webview/java/io/coinos/app/MainActivity.java | 22 | ||||
| -rw-r--r-- | src/features/messages.js | 26 | ||||
| -rw-r--r-- | src/features/nwc.js | 2 | ||||
| -rw-r--r-- | src/i18n.js | 2 | ||||
| -rw-r--r-- | static/graphene.apk | bin | 38840 -> 39508 bytes | |||
| -rw-r--r-- | tools/unifiedpush-test.js | 26 |
6 files changed, 74 insertions, 4 deletions
diff --git a/android/app/src/webview/java/io/coinos/app/MainActivity.java b/android/app/src/webview/java/io/coinos/app/MainActivity.java index 1c0c9d5..a7980c4 100644 --- a/android/app/src/webview/java/io/coinos/app/MainActivity.java +++ b/android/app/src/webview/java/io/coinos/app/MainActivity.java @@ -17,6 +17,8 @@ import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; +import java.util.List; + /** * The wallet, in a WebView this app owns. * @@ -108,10 +110,30 @@ public class MainActivity extends Activity { && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { try { requestPermissions(new String[] { Manifest.permission.POST_NOTIFICATIONS }, REQ_PERMS); } catch (Exception ignored) {} } + // The page can ask where it's running and whether anything on this phone + // can deliver a push — the advice it gives is useless otherwise ("use + // Firefox" makes no sense inside a WebView that IS the app). + web.addJavascriptInterface(new Host(), "CoinosHost"); Push.register(this); web.loadUrl(urlFor(getIntent())); } + /** What the page is allowed to ask about its surroundings. */ + private class Host { + @android.webkit.JavascriptInterface + public String env() { + String dist = Push.distributor(MainActivity.this); + List<String> all = Push.distributors(MainActivity.this); + String endpoint = Push.endpoint(MainActivity.this); + return "{\"app\":\"graphene\",\"distributors\":" + all.size() + + ",\"distributor\":" + (dist == null ? "null" : "\"" + dist + "\"") + + ",\"endpoint\":" + (endpoint != null) + "}"; + } + /** The wake job's bridge has this too; here it's a no-op. */ + @android.webkit.JavascriptInterface + public void done() {} + } + /** A scheme intent (lightning:, bitcoin:, nostr:) rides in as ?u=, like the TWA's. */ private String urlFor(Intent intent) { StringBuilder url = new StringBuilder(BASE); diff --git a/src/features/messages.js b/src/features/messages.js index 9fbd19b..b153dba 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -1284,6 +1284,25 @@ export function messagesFeature(ctx) { } catch {} })(); + // What to tell someone whose phone can't be woken. In a browser that's + // "your browser has no push service"; inside the UnifiedPush build of the + // Android app it's about the distributor, because that build IS the push — + // telling a WebView to go and use Firefox would be nonsense. + function pushAdvice() { + const env = hostEnv(); + if (!env || env.app !== 'graphene') return t('nwcNoPushService'); + if (!env.distributors) return t('upNoDistributor'); + if (!env.endpoint) return t('upWaiting', { app: (env.distributor || '').split('.').pop() }); + return t('nwcNoPushService'); + } + function hostEnv() { + try { + const h2 = typeof window !== 'undefined' && window.CoinosHost; + if (!h2 || typeof h2.env !== 'function') return null; + return JSON.parse(h2.env()); + } catch { return null; } + } + async function registerPush({ interactive = false } = {}) { try { // A UnifiedPush endpoint needs no permission and no push service: it's @@ -4038,9 +4057,10 @@ export function messagesFeature(ctx) { }, 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. + // wherever the user got to. Inside the Android build that carries its own + // push, the answer is a different one — see pushAdvice. if (st().noPushService && !st().push) - kids.push(h('div', { class: 'notice info small' }, t('nwcNoPushService'))); + kids.push(h('div', { class: 'notice info small' }, pushAdvice())); // ---- the feed, above the conversations: it's the thing you read, they're // the things you answer @@ -4548,6 +4568,8 @@ export function messagesFeature(ctx) { // Also the way a zap sent from a form reaches the tally right away, // without waiting on its receipt. zapSettled(eventId, ok, sats) { settleZap(eventId, ok, sats); return true; }, + // the right thing to say about push on THIS device, wherever it's asked + pushAdvice() { return pushAdvice(); }, // Chat lives behind a header button and takes over the whole screen — // no balance card, no tabs; each view carries its own way back. // The bare avatar node for the app header's identity menu. diff --git a/src/features/nwc.js b/src/features/nwc.js index 2cb9d81..656c254 100644 --- a/src/features/nwc.js +++ b/src/features/nwc.js @@ -884,7 +884,7 @@ export function nwcFeature(ctx) { list.length ? 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')) + ? h('div', { class: 'small faint' }, hook('pushAdvice') || t('nwcNoPushService')) : null, h('div', { class: 'row between' }, h('span', { class: 'small' }, t('nwcBackground')), diff --git a/src/i18n.js b/src/i18n.js index 38836d2..6f29a77 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -854,6 +854,8 @@ const STR = { nwcBackground: 'Answer while closed', nwcNoPushHere: 'This browser can\u2019t receive push messages.', nwcNoPermission: 'Notifications weren\u2019t allowed.', + upNoDistributor: 'To be reached while this app is closed you need a push app installed alongside it — ntfy is the usual one (F\u2011Droid or the Play Store). Install it, open it once, then reopen coinos. Until then, payments and messages arrive whenever coinos is open.', + upWaiting: 'Waiting for {app} to hand over a push address \u2014 reopen coinos in a moment and it should be here.', 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', diff --git a/static/graphene.apk b/static/graphene.apk Binary files differindex 29ca693..0688a40 100644 --- a/static/graphene.apk +++ b/static/graphene.apk diff --git a/tools/unifiedpush-test.js b/tools/unifiedpush-test.js index 6cf7b6e..022ead6 100644 --- a/tools/unifiedpush-test.js +++ b/tools/unifiedpush-test.js @@ -14,6 +14,7 @@ import puppeteer from 'puppeteer-core'; import { generateMnemonic } from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; import { buildHtml } from '../build.js'; +import { cacheKeyFor } from '../src/wallet.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; }; @@ -57,7 +58,8 @@ try { await click('get started'); await sleep(500); await click('import existing'); await sleep(400); await page.waitForSelector('textarea'); - await page.type('textarea', generateMnemonic(wordlist)); + const mn = generateMnemonic(wordlist); + await page.type('textarea', mn); await click('open wallet'); await waitText('receive', 20000); @@ -79,6 +81,28 @@ try { check('the browser\'s own dead push is no longer the story', !/no push service/i.test(txt), (txt.match(/[^\n]*push service[^\n]*/i) || [''])[0].slice(0, 50)); + // Inside the Android build, the advice about push is about the + // distributor, not about browsers — it IS the browser. + await page.evaluateOnNewDocument(() => { + window.CoinosHost = { env: () => JSON.stringify({ app: 'graphene', distributors: 0, distributor: null, endpoint: false }) }; + }); + // a phone that has never managed a push: no endpoint, and the browser's + // own push already known to be dead + await page.evaluate((k) => { + localStorage.removeItem('coinos-unifiedpush'); + const st = JSON.parse(localStorage.getItem(k) || '{}'); + st.push = false; st.noPushService = true; + localStorage.setItem(k, JSON.stringify(st)); + }, cacheKeyFor(mn + '\n') + ':messages'); + await page.goto('http://localhost:5263/', { waitUntil: 'domcontentloaded' }); + await waitText('receive', 20000); + await page.evaluate(() => { const b = [...document.querySelectorAll('button')].find((e) => /message/i.test(e.getAttribute('aria-label') || '')); if (b) b.click(); }); + await sleep(2500); + const advice = await page.evaluate(() => document.body.innerText); + check('with no distributor installed it says to install one', /ntfy/i.test(advice), + (advice.match(/[^\n]*ntfy[^\n]*/i) || ['nothing about a distributor'])[0].slice(0, 60)); + check('...and not to go and use Firefox', !/Firefox/.test(advice)); + // The WebView build wakes the wallet off-screen with ?wake=<payload> and a // CoinosHost bridge. The page has to notice, tidy the URL, and say when // it's finished so the service can stop. |
