• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

karanshukla / navyfragen-app / 28455186937

30 Jun 2026 03:15PM UTC coverage: 96.163% (-1.4%) from 97.563%
28455186937

push

github

web-flow
Merge pull request #191 from karanshukla/dev-push-notifications

Dev push notifications

2103 of 2293 branches covered (91.71%)

Branch coverage included in aggregate %.

880 of 988 new or added lines in 20 files covered. (89.07%)

4 existing lines in 4 files now uncovered.

7923 of 8133 relevant lines covered (97.42%)

6.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

26.67
/client/src/api/notificationService.ts
1
import { useMutation, useQuery } from "@tanstack/react-query";
2

3
import { apiClient, ApiError } from "./apiClient";
4

5
export type PushPermission = "default" | "granted" | "denied" | "unsupported";
6

7
export function getPushPermission(): PushPermission {
8
  if (typeof window === "undefined" || !("Notification" in window)) return "unsupported";
32!
NEW
9
  return Notification.permission as PushPermission;
×
10
}
11

12
const VAPID_PUBLIC_KEY_ENDPOINT = "/notifications/vapid-public-key";
2✔
13

14
async function fetchVapidPublicKey(): Promise<string | null> {
15
  try {
20✔
16
    const { vapidPublicKey } = await apiClient.get<{ vapidPublicKey: string }>(
20✔
17
      VAPID_PUBLIC_KEY_ENDPOINT
18
    );
19
    return vapidPublicKey;
1✔
20
  } catch (err) {
21
    if ((err as ApiError)?.status === 501) return null;
19!
22
    throw err;
19✔
23
  }
24
}
25

26
function pushError(message: string, status: number): ApiError {
NEW
27
  return { error: message, status };
×
28
}
29

30
function base64UrlToApplicationServerKey(base64url: string): Uint8Array<ArrayBuffer> {
NEW
31
  const base64 = base64url
×
32
    .replace(/-/g, "+")
33
    .replace(/_/g, "/")
34
    .padEnd(base64url.length + ((4 - (base64url.length % 4)) % 4), "=");
NEW
35
  const decoded = atob(base64);
×
NEW
36
  const bytes = new Uint8Array(new ArrayBuffer(decoded.length));
×
NEW
37
  for (let i = 0; i < decoded.length; i++) bytes[i] = decoded.charCodeAt(i);
×
NEW
38
  return bytes;
×
39
}
40

41
async function createPushSubscription(): Promise<string> {
NEW
42
  const vapidPublicKey = await fetchVapidPublicKey();
×
NEW
43
  if (!vapidPublicKey) {
×
NEW
44
    throw pushError("Push notifications are not available on this server", 501);
×
45
  }
46

NEW
47
  if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
×
NEW
48
    throw pushError("Push notifications are not supported by this browser", 501);
×
49
  }
50

NEW
51
  if ((await Notification.requestPermission()) !== "granted") {
×
NEW
52
    throw pushError("Notification permission was not granted", 403);
×
53
  }
54

NEW
55
  const registration = await navigator.serviceWorker.ready;
×
NEW
56
  const subscription = await registration.pushManager.subscribe({
×
57
    userVisibleOnly: true,
58
    applicationServerKey: base64UrlToApplicationServerKey(vapidPublicKey),
59
  });
60

NEW
61
  const { endpoint, keys } = subscription.toJSON();
×
NEW
62
  if (!endpoint) {
×
NEW
63
    throw pushError("Push subscription returned no endpoint", 502);
×
64
  }
65

NEW
66
  await apiClient.post("/notifications/subscribe", {
×
67
    endpoint,
68
    keys: { p256dh: keys?.p256dh, auth: keys?.auth },
69
  });
70

NEW
71
  return endpoint;
×
72
}
73

74
async function cancelPushSubscription(): Promise<void> {
NEW
75
  if (!("serviceWorker" in navigator)) return;
×
76

NEW
77
  const registration = await navigator.serviceWorker.ready;
×
NEW
78
  const subscription = await registration.pushManager.getSubscription();
×
NEW
79
  if (!subscription) return;
×
80

NEW
81
  const { endpoint } = subscription;
×
NEW
82
  await subscription.unsubscribe();
×
NEW
83
  await apiClient.delete("/notifications/subscribe", { endpoint });
×
84
}
85

86
// --- React Query hooks ---
87

88
export const notificationKeys = {
2✔
89
  vapid: ["notifications", "vapid"] as const,
90
};
91

92
export function usePushAvailable() {
93
  return useQuery<boolean>({
32✔
94
    queryKey: notificationKeys.vapid,
95
    queryFn: async () => (await fetchVapidPublicKey()) !== null,
20✔
96
    retry: false,
97
    refetchOnWindowFocus: false,
98
    staleTime: Infinity,
99
  });
100
}
101

102
export function useEnablePushNotifications() {
103
  return useMutation({ mutationFn: createPushSubscription });
32✔
104
}
105

106
export function useDisablePushNotifications() {
107
  return useMutation({ mutationFn: cancelPushSubscription });
32✔
108
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc