Skip to content

Commit a0eee06

Browse files
committed
Fix assignment notification enable flow
1 parent 0d51312 commit a0eee06

5 files changed

Lines changed: 73 additions & 19 deletions

File tree

src/apolloClient.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import { getMainDefinition } from '@apollo/client/utilities';
44
import { createClient } from 'graphql-ws';
55

66
const httpLink = createHttpLink({
7-
uri: import.meta.env.VITE_NOTIFYCOMP_API_ORIGIN || 'https://admin.notifycomp.com/graphql',
7+
uri: import.meta.env.VITE_NOTIFYCOMP_API_ORIGIN || 'https://api.notifycomp.com/api/graphql',
88
});
99

1010
const wsLink = new GraphQLWsLink(
1111
createClient({
12-
url: import.meta.env.VITE_NOTIFYCOMP_WS_ORIGIN || 'wss://admin.notifycomp.com/graphql',
12+
url: import.meta.env.VITE_NOTIFYCOMP_WS_ORIGIN || 'wss://api.notifycomp.com/api/graphql',
1313
}),
1414
);
1515

src/hooks/useAssignmentNotifications/useAssignmentNotifications.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { useCallback, useMemo, useState } from 'react';
1+
import { useCallback, useEffect, useMemo, useState } from 'react';
22
import {
33
AssignmentNotificationStatus,
44
disableAssignmentNotifications,
55
enableAssignmentNotifications,
66
getAssignmentNotificationStatus,
7+
isAssignmentNotificationsEnabled,
78
} from '@/lib/notifications/assignmentNotifications';
89

910
interface UseAssignmentNotificationsParams {
@@ -20,6 +21,12 @@ export function useAssignmentNotifications({
2021
);
2122
const [isSaving, setIsSaving] = useState(false);
2223
const [error, setError] = useState<string | null>(null);
24+
const [isEnabled, setIsEnabled] = useState(isAssignmentNotificationsEnabled);
25+
26+
useEffect(() => {
27+
setStatus(getAssignmentNotificationStatus());
28+
setIsEnabled(isAssignmentNotificationsEnabled());
29+
}, [user]);
2330

2431
const watches = useMemo(
2532
() =>
@@ -39,9 +46,11 @@ export function useAssignmentNotifications({
3946
try {
4047
const nextStatus = await enableAssignmentNotifications(watches);
4148
setStatus(nextStatus);
49+
setIsEnabled(isAssignmentNotificationsEnabled());
4250
} catch (e) {
4351
setError(e instanceof Error ? e.message : 'Unable to enable assignment notifications.');
4452
setStatus(getAssignmentNotificationStatus());
53+
setIsEnabled(isAssignmentNotificationsEnabled());
4554
} finally {
4655
setIsSaving(false);
4756
}
@@ -54,16 +63,18 @@ export function useAssignmentNotifications({
5463
try {
5564
await disableAssignmentNotifications();
5665
setStatus(getAssignmentNotificationStatus());
66+
setIsEnabled(isAssignmentNotificationsEnabled());
5767
} catch (e) {
5868
setError(e instanceof Error ? e.message : 'Unable to disable assignment notifications.');
69+
setIsEnabled(isAssignmentNotificationsEnabled());
5970
} finally {
6071
setIsSaving(false);
6172
}
6273
}, []);
6374

6475
return {
65-
canEnable: status === 'default' && watches.length > 0,
66-
canDisable: status === 'granted',
76+
canEnable: (status === 'default' || status === 'granted') && !isEnabled && watches.length > 0,
77+
canDisable: status === 'granted' && isEnabled,
6778
enable,
6879
disable,
6980
error,

src/lib/notifications/assignmentNotifications.ts

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import { getLocalStorage } from '@/lib/localStorage';
1+
import { deleteLocalStorage, getLocalStorage, setLocalStorage } from '@/lib/localStorage';
22

3-
const NOTIFY_COMP_ORIGIN = import.meta.env.VITE_NOTIFY_COMP_ORIGIN ?? '';
3+
const NOTIFY_COMP_ORIGIN =
4+
import.meta.env.VITE_NOTIFY_COMP_ORIGIN ?? 'https://api.notifycomp.com/api';
45
const NOTIFY_COMP_TOKEN_URL = '/.netlify/functions/notify-comp-token';
6+
const ENABLED_STORAGE_KEY = 'assignmentNotifications.enabled';
7+
const SERVICE_WORKER_TIMEOUT_MS = 10000;
58

69
interface PushSubscriptionJson {
710
endpoint?: string;
@@ -16,10 +19,13 @@ export interface AssignmentNotificationWatch {
1619
wcaUserId: number;
1720
}
1821

19-
export type AssignmentNotificationStatus = NotificationPermission | 'not-signed-in' | 'unsupported';
22+
export type AssignmentNotificationStatus = NotificationPermission | 'reauthorize' | 'unsupported';
2023

2124
const notifyCompUrl = (path: string) => `${NOTIFY_COMP_ORIGIN}${path}`;
2225

26+
export const isAssignmentNotificationsEnabled = () =>
27+
getLocalStorage(ENABLED_STORAGE_KEY) === 'true';
28+
2329
const getAccessToken = () => {
2430
const expiresAt = Number(getLocalStorage('expirationTime') ?? 0);
2531
const accessToken = getLocalStorage('accessToken');
@@ -54,7 +60,7 @@ export const getAssignmentNotificationStatus = (): AssignmentNotificationStatus
5460
}
5561

5662
if (!getAccessToken()) {
57-
return 'not-signed-in';
63+
return 'reauthorize';
5864
}
5965

6066
return Notification.permission;
@@ -72,10 +78,21 @@ export const requestAssignmentNotificationPermission = async () => {
7278
return await Notification.requestPermission();
7379
};
7480

81+
const readErrorMessage = async (response: Response) => {
82+
const text = await response.text();
83+
84+
try {
85+
const payload = JSON.parse(text) as { message?: string };
86+
return payload.message || text;
87+
} catch {
88+
return text;
89+
}
90+
};
91+
7592
const fetchNotifyCompToken = async () => {
7693
const accessToken = getAccessToken();
7794
if (!accessToken) {
78-
throw new Error('Sign in with WCA to enable assignment notifications.');
95+
throw new Error('Refresh your WCA authorization to enable assignment notifications.');
7996
}
8097

8198
const response = await fetch(NOTIFY_COMP_TOKEN_URL, {
@@ -87,7 +104,7 @@ const fetchNotifyCompToken = async () => {
87104
});
88105

89106
if (!response.ok) {
90-
throw new Error(await response.text());
107+
throw new Error(await readErrorMessage(response));
91108
}
92109

93110
const payload = (await response.json()) as { token?: string };
@@ -102,7 +119,7 @@ const fetchVapidPublicKey = async () => {
102119
const response = await fetch(notifyCompUrl('/v0/external/push/vapid-public-key'));
103120

104121
if (!response.ok) {
105-
throw new Error(await response.text());
122+
throw new Error(await readErrorMessage(response));
106123
}
107124

108125
const payload = (await response.json()) as { publicKey?: string };
@@ -113,8 +130,27 @@ const fetchVapidPublicKey = async () => {
113130
return payload.publicKey;
114131
};
115132

133+
const withTimeout = async <T>(promise: Promise<T>, message: string) =>
134+
await Promise.race([
135+
promise,
136+
new Promise<never>((_, reject) => {
137+
window.setTimeout(() => reject(new Error(message)), SERVICE_WORKER_TIMEOUT_MS);
138+
}),
139+
]);
140+
141+
const getServiceWorkerRegistration = async () => {
142+
if (import.meta.env.DEV) {
143+
return await navigator.serviceWorker.register('/notification-sw.js');
144+
}
145+
146+
return await withTimeout(
147+
navigator.serviceWorker.ready,
148+
'Notification service worker was not ready. Refresh the page and try again.',
149+
);
150+
};
151+
116152
const getPushSubscription = async () => {
117-
const registration = await navigator.serviceWorker.ready;
153+
const registration = await getServiceWorkerRegistration();
118154
const existing = await registration.pushManager.getSubscription();
119155

120156
if (existing) {
@@ -133,7 +169,8 @@ export const enableAssignmentNotifications = async (watches: AssignmentNotificat
133169
return permission;
134170
}
135171

136-
const [token, subscription] = await Promise.all([fetchNotifyCompToken(), getPushSubscription()]);
172+
const token = await fetchNotifyCompToken();
173+
const subscription = await getPushSubscription();
137174
const payload = subscription.toJSON() as PushSubscriptionJson;
138175

139176
if (!payload.endpoint || !payload.keys?.p256dh || !payload.keys.auth) {
@@ -155,17 +192,20 @@ export const enableAssignmentNotifications = async (watches: AssignmentNotificat
155192
});
156193

157194
if (!response.ok) {
158-
throw new Error(await response.text());
195+
deleteLocalStorage(ENABLED_STORAGE_KEY);
196+
throw new Error(await readErrorMessage(response));
159197
}
160198

199+
setLocalStorage(ENABLED_STORAGE_KEY, 'true');
161200
return permission;
162201
};
163202

164203
export const disableAssignmentNotifications = async () => {
165-
const registration = await navigator.serviceWorker.ready;
204+
const registration = await getServiceWorkerRegistration();
166205
const subscription = await registration.pushManager.getSubscription();
167206

168207
if (!subscription) {
208+
deleteLocalStorage(ENABLED_STORAGE_KEY);
169209
return;
170210
}
171211

@@ -184,4 +224,5 @@ export const disableAssignmentNotifications = async () => {
184224
}
185225

186226
await subscription.unsubscribe();
227+
deleteLocalStorage(ENABLED_STORAGE_KEY);
187228
};

src/pages/Settings/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,11 @@ export default function Settings() {
7272
<p className="type-meta">This browser does not support push notifications.</p>
7373
)}
7474

75-
{user && notifications.status === 'not-signed-in' && (
75+
{user && notifications.status === 'reauthorize' && (
7676
<div className="space-y-2">
77-
<p className="type-meta">Sign in with WCA again to enable notifications.</p>
77+
<p className="type-meta">Refresh your WCA authorization to enable notifications.</p>
7878
<Button type="button" onClick={signIn}>
79-
Sign in with WCA
79+
Continue with WCA
8080
</Button>
8181
</div>
8282
)}

src/vite-env.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ declare const __GIT_TAG__: string;
66

77
interface ImportMetaEnv {
88
readonly VITE_NOTIFY_COMP_ORIGIN?: string;
9+
readonly VITE_NOTIFYCOMP_API_ORIGIN?: string;
10+
readonly VITE_NOTIFYCOMP_WS_ORIGIN?: string;
911
}

0 commit comments

Comments
 (0)