aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--android/app/src/webview/java/io/coinos/app/MainActivity.java22
-rw-r--r--src/features/messages.js26
-rw-r--r--src/features/nwc.js2
-rw-r--r--src/i18n.js2
-rw-r--r--static/graphene.apkbin38840 -> 39508 bytes
-rw-r--r--tools/unifiedpush-test.js26
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
index 29ca693..0688a40 100644
--- a/static/graphene.apk
+++ b/static/graphene.apk
Binary files differ
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.