aboutsummaryrefslogtreecommitdiffstats
path: root/src/app.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-04 20:48:27 -0700
committerAdam Soltys <[email protected]>2026-09-04 20:48:27 -0700
commit8d27a5d2c59e0f1824753d59a7326b6b76887bf7 (patch)
tree5894488403d2f72b994255ba8ff6859121ead1d4 /src/app.js
parent175600238fb3e9cf60bd65b0164ced476d18f445 (diff)
downloadcoinosv3-8d27a5d2c59e0f1824753d59a7326b6b76887bf7.tar.gz
coinosv3-8d27a5d2c59e0f1824753d59a7326b6b76887bf7.zip
Android wrapper app (android/): a minimal TWA around v3.coinos.io whose whole reason to exist is intent registration — lightning:/bitcoin:/nostr:/lnurl:/lnurlw:/lnurlp: VIEW intents and the same schemes via NFC NDEF dispatch (bolt cards, address stickers; NFC declared required=false), plus verified https links to the site. LauncherActivity forwards a scheme URI into the web app as ?u=<encoded>; the web app reads it BEFORE features construct — a nostr npub/nprofile rewrites itself onto the existing /npub profile deep link, payment URIs stash and dispatch through handleScanned (the QR path) once a wallet is open, and the param is stripped from the URL either way. assetlinks.json (static/.well-known/, copied by build.js) vouches for the signing cert so the TWA runs URL-bar-less; the built APK is served at /coinos.apk for sideloading. Signing keystore lives in android/ GITIGNORED (keystore.properties beside it) — the assetlinks fingerprint pins it, so losing it means re-shipping assetlinks too. Verified: headless ?u= routing (lightning → Send with invoice filled + URL cleaned; nostr → profile page), APK installed on a real phone via adb, schemes registered (chooser appears where other wallets also claim lightning:)
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
Diffstat (limited to 'src/app.js')
-rw-r--r--src/app.js32
1 files changed, 32 insertions, 0 deletions
diff --git a/src/app.js b/src/app.js
index c815cc5..1343c6f 100644
--- a/src/app.js
+++ b/src/app.js
@@ -4850,6 +4850,21 @@ const ctx = {
if (restored) { ui.onb = null; try { localStorage.removeItem(ONB_STEP_KEY); } catch {} }
},
};
+// Android wrapper intents: the TWA app (android/) registers for lightning:/
+// bitcoin:/nostr:/lnurl: URIs and forwards them here as ?u=<encoded>. Read
+// BEFORE features construct: a nostr npub/nprofile rewrites itself onto the
+// existing /npub… profile deep link (which the messages feature reads at
+// construction); payment URIs stash for dispatch once a wallet is open.
+const INTENT_URI = (() => {
+ try {
+ const u = new URLSearchParams(location.search).get('u');
+ if (!u) return null;
+ history.replaceState(null, '', location.pathname || '/');
+ const m = u.match(/^nostr:((npub|nprofile)1[a-z0-9]+)\/?$/i);
+ if (m) { history.replaceState(null, '', '/' + m[1]); return null; }
+ return u;
+ } catch { return null; }
+})();
const FEATURES = buildFeatures(ctx);
// Deferred features (gifts, NWC, hats) hold placeholder slots in FEATURES —
// hook precedence is position — and arrive as separate chunks right after
@@ -4956,6 +4971,23 @@ document.addEventListener('visibilitychange', () => {
// Claim a stashed migrated name as soon as a wallet is actually open — a
// vault unlock or slow boot can put that minutes after 'load'. The stash
// survives reloads, so an unfinished claim resumes on the next visit too.
+// Dispatch a stashed payment intent (lightning:/bitcoin:/lnurl: from the
+// Android wrapper) once a wallet is actually open — a locked vault or slow
+// boot can put that well after load. handleScanned routes it exactly like a
+// scanned QR: bolt11s and lnurls through the feature chain, bitcoin: through
+// BIP-21 (nostr profile intents already rerouted onto the /npub deep link).
+if (INTENT_URI) {
+ const started = Date.now();
+ const tick = setInterval(() => {
+ if (Date.now() - started > 10 * 60_000) return clearInterval(tick);
+ if (!activeAccount() || !wallet.loaded || ui.screen !== 'wallet') return;
+ clearInterval(tick);
+ ui.tab = 'send';
+ render();
+ handleScanned(INTENT_URI);
+ }, 500);
+}
+
window.addEventListener('load', () => {
if (!pendingMigratedName()) return;
const started = Date.now();