Skip to content

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

Open
Tijesunimi004 wants to merge 1 commit into
codebestia:mainfrom
Tijesunimi004:feat/push-sw
Open

feat: web service worker + push subscription flow (#240)#273
Tijesunimi004 wants to merge 1 commit into
codebestia:mainfrom
Tijesunimi004:feat/push-sw

Conversation

@Tijesunimi004

Copy link
Copy Markdown

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
@drips-wave

drips-wave Bot commented Jun 28, 2026

Copy link
Copy Markdown

Hey @Tijesunimi004! 👋 It looks like this PR isn't linked to any issue.

If this PR is for one of the issues assigned to you as part of a Wave, please link it to ensure your contribution is tracked properly. You can do this by adding a keyword to the PR description (e.g., Closes #123), or by clicking a button below:

Issue Title
#240 Web service worker + subscription flow (apps/web) Link to this issue
#194 Delivery pipeline: validate → persist → resolve devices → push Link to this issue

ℹ️ Learn more about linking PRs to issues

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.

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.

2 participants