Skip to content

feat: web service worker + push subscription flow (#240)#273

Merged
codebestia merged 5 commits into
codebestia:mainfrom
Tijesunimi004:feat/push-sw
Jun 29, 2026
Merged

feat: web service worker + push subscription flow (#240)#273
codebestia merged 5 commits into
codebestia:mainfrom
Tijesunimi004:feat/push-sw

Conversation

@Tijesunimi004

@Tijesunimi004 Tijesunimi004 commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Closes #240


What

Implements the full client-side push notification flow in apps/web per #240.

Files

apps/web/public/sw.js (new)

Plain-JS service worker served at /sw.js. Three event handlers:

  • installskipWaiting() so updates activate immediately.
  • activateclients.claim() so the SW controls all open tabs.
  • push → shows a content-free notification. The body is always "You have a new message"; no message ciphertext is ever shown. Tagged by conversationId so repeated pushes for the same thread collapse rather than stack.
  • notificationclick → closes the notification, then either posts sw:sync to an already-open window and calls client.focus(), or opens a new tab at /app/conversations/{id}. Never calls client.navigate() (only valid on controlled clients).

apps/web/src/hooks/usePushSubscription.ts (new)

usePushSubscription(token) hook:

  1. Registers /sw.js on mount (guarded for SSR, no serviceWorker, and no PushManager).
  2. On mount, if permission is already granted, re-POSTs any existing subscription to keep the server in sync.
  3. Exposes requestSubscription() — the only path that calls Notification.requestPermission(). Called only when the user explicitly clicks "Enable". Never called eagerly.
  4. On grant: subscribes with NEXT_PUBLIC_VAPID_PUBLIC_KEY, then POST /push/subscriptions with Authorization: Bearer <token>.

apps/web/src/components/PushPermissionPrompt.tsx (new)

Dismissible banner:

  • Waits 5 s after mount before appearing (contextual — not on first load).
  • Hidden when Notification.permission is granted or denied, when already subscribed, or when dismissed.
  • Dismissal recorded in sessionStorage so it stays hidden for the session.
  • "Enable" button calls requestSubscription(); "Not now" hides for the session.

apps/web/src/app/app/layout.tsx (modified)

  • Mounts <PushPermissionPrompt /> at the bottom of the authenticated shell.
  • Adds a navigator.serviceWorker message listener that handles sw:sync events from the SW's notificationclick handler, navigating the Next.js router to the relevant conversation (triggering a data sync).

Acceptance criteria

  • SW registers + subscribes after user grants permission: usePushSubscription registers the SW on mount and subscribes only when requestSubscription() is called (after the user clicks "Enable" in the prompt).
  • Subscription persisted server-side: postSubscription() POSTs the PushSubscription JSON to POST /push/subscriptions with the user's JWT.
  • Clicking the notification opens the conversation and syncs: the SW's notificationclick posts sw:sync to the open client; the layout's message listener navigates to /app/conversations/{id}, which triggers the page's data fetching hooks.

Environment variable required

NEXT_PUBLIC_VAPID_PUBLIC_KEY=<base64url-encoded VAPID public key>

Adds the full client-side push notification flow for apps/web:

public/sw.js
- Registers with skipWaiting/clients.claim so updates take effect immediately.
- push handler: shows a content-free notification (body: "You have a new
  message") tagged by conversationId so duplicates are collapsed.
- notificationclick: posts sw:sync to an existing window or opens a new
  tab at /app/conversations/{id}. Never displays message ciphertext.

src/hooks/usePushSubscription.ts
- Registers /sw.js on mount (browser support guarded).
- Exposes requestSubscription() — the only function that calls
  Notification.requestPermission(); never called eagerly on page load.
- On grant, calls pushManager.subscribe() with the NEXT_PUBLIC_VAPID_PUBLIC_KEY
  applicationServerKey and POSTs the result to /push/subscriptions with the
  user's JWT in the Authorization header.
- Re-uses an existing subscription on re-mount to keep the server in sync.

src/components/PushPermissionPrompt.tsx
- Shown 5 s after the user enters the /app shell (contextual, not on load).
- Suppressed when permission is already granted/denied or when dismissed.
- Dismissal is recorded in sessionStorage so the banner stays gone for the
  session without permanently refusing the browser prompt.

src/app/app/layout.tsx
- Mounts <PushPermissionPrompt />.
- Adds a navigator.serviceWorker "message" listener that reacts to sw:sync
  events by navigating the router to the notified conversation, triggering
  a data reload.
Copilot AI review requested due to automatic review settings June 28, 2026 19:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@codebestia

Copy link
Copy Markdown
Owner

@Tijesunimi004 Please fix the CI

@codebestia

Copy link
Copy Markdown
Owner

Also don't forget to link your issue.

@drips-wave

drips-wave Bot commented Jun 29, 2026

Copy link
Copy Markdown

@Tijesunimi004 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Tijesunimi004 and others added 4 commits June 29, 2026 09:12
The react-hooks/set-state-in-effect rule disallows calling setState
directly in an effect body. Notification.permission does not change
based on service worker registration, so a lazy state initializer
captures the initial value without needing a separate effect.

@codebestia codebestia left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

LGTM!
Thank you for your contribution.

@codebestia codebestia merged commit a5400fe into codebestia:main Jun 29, 2026
2 checks passed
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.

Web service worker + subscription flow (apps/web)

3 participants