Skip to content

Add Android UnifiedPush support#1307

Open
chyendongnhanh338 wants to merge 4 commits into
getpaseo:mainfrom
chyendongnhanh338:feat/unified-push-design
Open

Add Android UnifiedPush support#1307
chyendongnhanh338 wants to merge 4 commits into
getpaseo:mainfrom
chyendongnhanh338:feat/unified-push-design

Conversation

@chyendongnhanh338
Copy link
Copy Markdown

@chyendongnhanh338 chyendongnhanh338 commented Jun 3, 2026

Linked issue

Closes #1292

Type of change

  • Bug fix
  • New feature (with prior issue + design alignment)
  • Refactor / code improvement
  • Docs

What does this PR do

Adds Android UnifiedPush support without depending on expo-unified-push.

  • Adds Web Push subscription protocol messages, client RPCs, and daemon session handlers.
  • Stores typed push subscriptions, migrates legacy Expo token files, manages VAPID keys, and validates Web Push endpoints before sending.
  • Adds an in-repo Android Expo module for UnifiedPush registration and notification delivery.
  • Gates Android UnifiedPush registration on server_info.features.unifiedPush and the advertised VAPID public key while preserving Expo Push for iOS and unsupported hosts.
  • Updates the root lockfile and docs for the new subscription store, capability, and Android requirements.

How did you verify it

Ran the focused UnifiedPush and push notification test set:

node node_modules/vitest/vitest.mjs run packages/protocol/src/messages.push-subscription.test.ts packages/client/src/daemon-client.test.ts packages/server/src/server/push/token-store.test.ts packages/server/src/server/push/vapid-keypair.test.ts packages/server/src/server/push/endpoint-security.test.ts packages/server/src/server/push/push-service.test.ts packages/server/src/server/websocket-server.relay-reconnect.test.ts packages/server/src/server/session.test.ts packages/app/src/push/unified-push-shared.test.ts --bail=1

Observed: 9 test files passed, 202 tests passed.

Ran required repository checks:

npm run format
npm run typecheck
npm run lint
npm run format:check
npm run build:client
npm run build:server

Observed: all commands exited successfully. npm run lint reported 0 warnings and 0 errors. npm run format:check reported all matched files use the correct format.

Checked that the external package was not introduced:

rg -n "expo-unified-push" package-lock.json packages docs

Observed: no matches.

Checklist

  • One focused change. Unrelated cleanups split out.
  • npm run typecheck passes
  • npm run lint passes
  • npm run format ran (Biome)
  • No UI changes requiring screenshots or video
  • Tests added or updated where it made sense

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Jun 3, 2026

Greptile Summary

This PR adds Android UnifiedPush support by implementing a Web Push subscription protocol (register/unregister RPCs), VAPID keypair management, an SSRF-safe endpoint validator, a versioned push subscription store with legacy-migration, and an in-repo Expo Kotlin module that bridges the UnifiedPush connector to the React Native layer.

  • Server: token-store.ts migrates from a flat Expo token set to a typed PushSubscription discriminated union; vapid-keypair.ts loads or generates a persistent VAPID keypair; push-service.ts fans notifications out to both Expo and Web Push transports; two new dotted-namespace RPC handlers land in session.ts.
  • Client/app: New unified-push-shared.ts concentrates platform-independent event handling and payload parsing; unified-push.android.ts wires the Kotlin module to the daemon client; use-push-token-registration.ts gates Android UnifiedPush on the server-advertised VAPID key while leaving Expo Push intact for iOS.
  • Android module: PaseoUnifiedPushModule.kt and PaseoUnifiedPushService.kt implement the Expo native module and background push service, handling distributor selection, device registration, message delivery, and system notification display.

Confidence Score: 4/5

Safe to merge after fixing the uncaught NameNotFoundException in the Kotlin module.

The Kotlin module calls PackageManager.getPackageInfo and getApplicationIcon inside getDistributors without a try-catch; a NameNotFoundException propagates uncaught to JS where registerUnifiedPush has no surrounding catch, silently breaking Android registration.

PaseoUnifiedPushModule.kt and PaseoUnifiedPushService.kt

Important Files Changed

Filename Overview
packages/app/modules/paseo-unified-push/android/src/main/java/sh/paseo/unifiedpush/PaseoUnifiedPushModule.kt New Expo Kotlin module for UnifiedPush; getDistributors calls getPackageName/getDistributorIcon without catching NameNotFoundException, which can crash the entire registration flow.
packages/app/modules/paseo-unified-push/android/src/main/java/sh/paseo/unifiedpush/PaseoUnifiedPushService.kt New background push service; shows system notifications in foreground (when eventSink is set) and uses a truncating toInt() for notification IDs that can collide or go negative.
packages/server/src/server/push/push-service.ts Expanded to route Expo and Web Push subscriptions to separate transports; VAPID config, endpoint SSRF validation, and 404/410 cleanup are all handled correctly.
packages/server/src/server/push/token-store.ts Clean migration from a flat token set to a typed subscription list; legacy file format parsed and migrated on first load.
packages/server/src/server/push/endpoint-security.ts New SSRF guard for Web Push endpoints; IPv4-mapped IPv6 handling was flagged in a prior review thread.
packages/app/src/push/unified-push-shared.ts Platform-independent helpers for UnifiedPush event handling, subscription normalization, and payload parsing; well-tested with pure functions.
packages/app/src/push/unified-push.android.ts Android-specific registration and event subscription wiring; lacks a try-catch around getDistributors.
packages/app/src/hooks/use-push-token-registration.ts Android UnifiedPush gated on unifiedPushVapidPublicKey; re-registration on every serverInfo update was flagged in a prior review thread.
packages/server/src/server/session.ts Two new message handlers added cleanly; incoming messages are schema-validated before dispatch.
packages/server/src/server/websocket-server.ts VAPID keypair loaded at startup and threaded into push sender; server_info now always emits capabilities.pushNotifications.webPushVapidPublicKey.
packages/protocol/src/messages.ts New Zod schemas and types for Web Push subscription RPC and server capabilities; inlined into the existing discriminated union correctly.

Sequence Diagram

sequenceDiagram
    participant App as Android App (JS)
    participant Module as PaseoUnifiedPushModule (Kotlin)
    participant Service as PaseoUnifiedPushService (Kotlin)
    participant UP as UnifiedPush Distributor
    participant Server as Paseo Server
    participant Web as Web Push Gateway

    App->>Server: WebSocket connect
    Server-->>App: "server_info { features.unifiedPush, webPushVapidPublicKey }"
    App->>Module: getDistributors()
    Module-->>App: distributors list
    App->>Module: saveDistributor + registerDevice(vapidPublicKey)
    Module->>UP: UnifiedPush.register(vapidPublicKey)
    UP-->>Service: onNewEndpoint(endpoint, pubKey, auth)
    Service-->>Module: eventSink registered event
    Module-->>App: "message { action: registered }"
    App->>Server: push.subscription.register.request
    Server-->>App: push.subscription.register.response

    Note over Server,Web: On agent completion
    Server->>Web: Web Push notification (VAPID-signed)
    Web->>UP: Deliver to device
    UP->>Service: onMessage(decrypted payload)
    Service-->>Module: eventSink message event
    Module-->>App: "message { action: message }"
Loading

Reviews (2): Last reviewed commit: "fix: address unified push review feedbac..." | Re-trigger Greptile

Comment thread packages/server/src/server/push/endpoint-security.ts
Comment thread packages/server/src/server/push/push-service.ts Outdated
Comment thread packages/server/src/server/push/push-service.ts Outdated
Comment thread packages/server/src/server/session.test.ts
Comment thread packages/app/src/hooks/use-push-token-registration.ts
Comment on lines +41 to +42
"name" to if (isInternal) "Internal FCM Distributor" else getPackageName(distributor),
"icon" to getDistributorIcon(distributor),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Both getPackageName and getDistributorIcon call PackageManager APIs that throw PackageManager.NameNotFoundException when a package isn't found — but neither is wrapped in a try-catch. They're both called inside the getDistributors lambda, so any race between enumeration and lookup (package uninstalled between the two calls) would propagate the exception up through the Expo Function block and surface as a JS error in registerUnifiedPush, causing the entire UnifiedPush registration flow to fail silently.

Suggested change
"name" to if (isInternal) "Internal FCM Distributor" else getPackageName(distributor),
"icon" to getDistributorIcon(distributor),
"name" to if (isInternal) "Internal FCM Distributor" else runCatching { getPackageName(distributor) }.getOrNull(),
"icon" to runCatching { getDistributorIcon(distributor) }.getOrNull(),

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add UnifiedPush support for Android push notifications (no FCM dependency)

1 participant