From a846211262f37238c001e5210938749294f8db6a Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 12 Sep 2026 10:05:10 -0700 Subject: UnifiedPush: reach a phone that has no Google in it. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx --- android/app/src/main/res/values/strings.xml | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'android/app/src/main/res/values/strings.xml') diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 8a4876a..8eca245 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,6 +1,13 @@ Coinos + + Something happened in your wallet + You were paid + New message + New message in a group + An app is asking to pay — open Coinos to approve [{ -- cgit v1.2.3