| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
app, the origin fails closed; 1.9 (code 10).
F-Droid review of 1.8: the shared manifest declared the TWA's
LauncherActivity/IntentActivity in the webview and fdroid builds, which
don't compile them (a second, crashing launcher icon). They move to
src/twa/AndroidManifest.xml. WakeJob's off-screen WebView now answers the
origin from assets like MainActivity's, and Assets returns 404 for any
v3.coinos.io path it doesn't carry outside the live API list instead of
fetching it. POST_NOTIFICATIONS is asked only when a UnifiedPush
distributor is installed. The description names the third-party hosts
(DoH, post media).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
colour follows the app's.
One dark colour for both bars gave white navigation icons over a light page
under gesture navigation. The wrapper (1.9, versionCode 10) now uses the
page background in light mode and the dark theme colour in dark mode, for
the status bar and the navigation bar alike. The page updates its
theme-color meta when its own theme is applied, so a PWA's status bar and
the wrapper's follow the same switch.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VhUKyXKtsJ67QAFfb7QR9D
|
| |
|
|
|
|
|
|
| |
F-Droid's APK check refuses the Play-only dependency-metadata signing
block AGP adds by default. Off for every flavor.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JuwQqNWtDJcxPWtEm9134z
|
| |
|
|
|
|
|
|
|
|
|
|
| |
From the fdroiddata review and its pipeline: store text (summary,
description, icon, changelogs) belongs in fastlane/ here rather than in
the recipe; the category is "Wallet"; the scanner strips committed APKs
by itself, so the two scandelete entries go; and bun.lockb is a binary
the scanner flags — replaced by Bun's text lockfile (bun.lock, same
pinned versions). A new tag is needed for the new tree, so 1.7.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JuwQqNWtDJcxPWtEm9134z
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The Android builds so far were shells around the live site: a verified
APK proved nothing about the code that ran. The new `fdroid` flavor
(io.coinos.fdroid) shares the WebView shell and carries the built web
app in its assets. The page still lives at the v3.coinos.io origin — same
storage, links and gift URLs — but every file under it is answered from
the APK (Assets.java); only the site's live API paths reach the network,
and the bundled page registers no service worker (HAL_NO_SW=1).
Two clean unsigned builds give the same APK. COINOS_UNSIGNED=1 builds
without the keystore; Bun is pinned via packageManager; the Gradle wrapper
properties pin 8.12. fdroid/io.coinos.fdroid.yml is the fdroiddata recipe
(checksum-pinned Bun in sudo, prebuild web build, scandelete, our signing
key allowed for the Binaries match) and docs/REPRODUCIBLE.md the how-to.
Verified on a phone: the bundled build cold-starts in airplane mode.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JuwQqNWtDJcxPWtEm9134z
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A push sent to ntfy.sh reached a killed app on a real phone:
17:02:21.259 ntfy.sh → the phone's ntfy → broadcast to io.coinos.webview
17:02:21.308 app process started (it had been killed)
17:02:21.458 wake: job started
17:02:22.926 wake: finishing (page said so) — 1.7s, and "You were paid"
One thing that took two goes: `adb shell am force-stop` marks a package
STOPPED, and Android delivers no broadcasts to a stopped package unless the
sender sets FLAG_INCLUDE_STOPPED_PACKAGES — which `am broadcast` does and a
real distributor rightly doesn't. The first attempt looked like a failure and
was a test artefact; `am kill`, which is what Android itself does to a
backgrounded app, delivers fine.
And the WebView build can now say something useful about push. It was
inheriting the browser's line — "your browser has no push service, try
Firefox" — which is nonsense inside a WebView that IS the app. It asks the
host what's actually there and says: install a push app like ntfy (nothing
installed), or wait a moment and reopen (a distributor that hasn't answered
yet). Payments and messages still arrive whenever coinos is open, which is
the part that matters most to someone reading an apology.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
ntfy went onto a real phone and the handshake ran: our app broadcast REGISTER,
ntfy answered with an endpoint (https://ntfy.sh/up…), the app stored it, and
the web app took it off the launch URL and kept it. That was the link I'd said
was unproven.
It also turned up something the sandbox never would. The phone had THREE apps
answering the distributor action — Element's and Yakihonne's internal ones
alongside ntfy — and both of those exist to serve their own app, not anyone
else's. Picking the first would have registered with a distributor that will
never hand us an endpoint, and there'd be nothing to show for it.
The spec's answer is to let the user choose, and this app has no settings
screen to choose in, so it works through them instead: ask one, and if no
endpoint has arrived by the next launch, ask the next. A refusal moves on
immediately. Whoever answers is remembered for good.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The whole chain, on real hardware, with the shipping APK installed:
am broadcast → PushReceiver → expedited job → off-screen WebView →
v3.coinos.io/?wake= → the wallet restored from its vault → CoinosHost.done()
15:49:52.341 schedule -> 1
15:49:52.390 job started (49ms later)
15:49:56.602 finishing (page said so)
Four and a bit seconds from a force-stopped app to the page reporting it had
dealt with the payload — not the timeout, the page. A notify payload raised
the right notification too ("You were paid", "New message"), and the wallet
it woke was one imported into the WebView build minutes earlier, restored
from the vault in a process that had been killed.
The payload no longer goes to logcat. It carries the user's push, and
anything with ADB can read that.
Unproven, and the one link I couldn't close here: the distributor handshake.
There's no UnifiedPush distributor on this phone, so every push above was a
broadcast I sent by hand — which is exactly what a distributor sends, but
it isn't the same as watching ntfy do it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Tested on a real phone, and the first attempt didn't work at all:
Background started FGS: Disallowed [... code:DENIED ...]
startForegroundService() not allowed due to mAllowStartForeground false
Android 12 forbids starting a foreground service from a broadcast receiver.
An expedited job is the sanctioned replacement — it runs promptly from the
background and needs no notification of its own to apologise for existing —
so WakeService is now WakeJob, and the notification is decided by what
arrived rather than by whether we could wake: a payment or a message is news
either way, a wallet-connect request the wallet answers by itself is not.
Two more things the phone found. A job with a connectivity constraint needs
ACCESS_NETWORK_STATE, or schedule() throws and nothing runs at all. And a
wake with nothing to answer used to hold a WebView open for the full thirty
seconds — a locked wallet, no wallet on the device, or no NWC connections all
end the job immediately now.
Also a bug worth naming: ctx carried `wakeDone` by shorthand, which captured
the binding while it was still undefined (ctx is built before it's defined).
The page could never say it had finished, so every wake ran its full timeout.
It reads through a closure now, like its neighbour.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Web push on Android is Firebase's, so a phone without Google Play Services —
GrapheneOS, CalyxOS, any de-Googled Android — cannot be woken by a web app at
all. UnifiedPush is the way that doesn't involve Google: the user runs a
distributor (ntfy and friends), apps ask it for an endpoint, and whoever
wants to reach the device POSTs to that endpoint.
THE WRAPPER speaks the protocol directly — four broadcasts each way, so it
pulls in no library; an APK this small has no business growing a Kotlin
runtime for eight intents. It asks the distributor on every launch (the call
is idempotent, and it's how a distributor installed later gets noticed),
raises a notification when a message arrives, and acknowledges it so the
distributor stops resending.
THE ENDPOINT reaches the web app on the launch URL as ?up=, the same road
payment intents already travel — because the distributor talks to the app and
only the web app knows which pubkeys are worth watching. It's registered with
the notifier in place of a browser subscription, for notifications and for
NWC alike, and needs neither permission nor push service.
THE NOTIFIER sends to either kind: one deliver() that POSTs the payload to a
UnifiedPush endpoint, or hands it to web-push as before. Same payloads, same
404/410 pruning. The endpoint is the only secret there, so it's never logged
in full.
Verified: a POST to a live ntfy endpoint delivers the payload byte for byte,
so the app parses exactly what the notifier sent; the APK builds, carries the
receiver, and is signed with the certificate assetlinks already pins (v1.6).
tools/unifiedpush-test.js drives a browser with no push service and checks
the endpoint is taken off the URL, kept, and registered.
Honest about the limit: a UnifiedPush message wakes the ANDROID app, not
Chrome's service worker, and the wallet's keys live in the browser's storage
where nothing here can reach them. So a zap request arrives as a notification
you tap, and the wallet answers when it opens — one tap instead of none.
Automatic answering while closed still needs a browser with a push service.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
| |
androidbrowserhelper defaults the TWA status/navigation bars to white when no color is declared — the PWA doesn't hit this because it uses the manifest theme_color. Added STATUS_BAR_COLOR / NAVIGATION_BAR_COLOR meta-data (light + dark variants) pointing at new color resources set to the PWA's theme_color #15171a, so the top bar matches the dark app chrome. Bumped to versionCode 6 / versionName 1.5 and rebuilt static/coinos.apk (meta-data verified present via aapt).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
discoveries via on-device screenshot comparison: (1) Chrome's maskable handling ZOOMS the PWA icon so far that the logo's outer ring is cropped away entirely — the PWA tile is actually the middle ring + half-moon only, so the adaptive foreground now renders from icon-inner.svg (the icon.svg minus its white disc and outer ring); (2) this launcher displays ~91% of the adaptive layer rather than the spec's 66% viewport, so the size was tuned empirically against side-by-side screenshots (three iterations: 61% too small, 84% too big, 77% matches). Published as 1.4 on zapstore; versionCode 5
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
matches the PWA exactly — the PWA's "maskable" icon turned out to be the plain transparent-background logo that Chrome plates on white, so the APK now ships a proper adaptive icon doing the same (white background layer, logo scaled into the 66% safe zone, launcher applies its mask); legacy mipmaps regenerated to match. Published to zapstore as release 1.2 and installed END-TO-END from the store on a real phone: the phone's Zapstore 0.2.7 couldn't read zsp's event schema ("app no longer exists") — updated to 1.1.2 and the flow ran clean: listing under the recognized coinos profile → Trust dialog → Play Protect first-seen scan → installed → launches mainnet v3 beta fullscreen
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
(IntentActivity, noHistory) — a TWA already on screen ignores a new intent delivered to its still-alive launcher, so a second lightning: tap did nothing; the trampoline maps the URI to ?u= form and relaunches the TWA with CLEAR_TOP, which navigates the open app. Verified on a real phone over adb: URL-bar-less fullscreen (assetlinks verified after the site began serving /.well-known/assetlinks.json — the deploy hook's dist/* glob was blind to dot-directories, fixed on cs), lightning: intent lands on Send with the invoice filled, and a second bitcoin: intent fired while the app was open navigated it (BIP-21 parsed, address + amount). NFC NDEF filters ride the trampoline for bolt cards / NDEF tags
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
|
|
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
|