From 0d513123149e21434b8fac5a0f484bd87428529a Mon Sep 17 00:00:00 2001 From: Cailyn Sinclair Date: Mon, 11 May 2026 15:06:22 -0700 Subject: [PATCH 1/2] integrate notifycomp assignment push --- netlify/functions/notify-comp-token.js | 96 +++++++++ public/notification-sw.js | 34 ++++ src/hooks/useAssignmentNotifications/index.ts | 1 + .../useAssignmentNotifications.ts | 74 +++++++ .../notifications/assignmentNotifications.ts | 187 ++++++++++++++++++ src/pages/Settings/index.tsx | 121 +++++++++--- src/providers/AuthProvider/AuthProvider.tsx | 5 + src/vite-env.d.ts | 4 + vite.config.ts | 3 + 9 files changed, 501 insertions(+), 24 deletions(-) create mode 100644 netlify/functions/notify-comp-token.js create mode 100644 public/notification-sw.js create mode 100644 src/hooks/useAssignmentNotifications/index.ts create mode 100644 src/hooks/useAssignmentNotifications/useAssignmentNotifications.ts create mode 100644 src/lib/notifications/assignmentNotifications.ts diff --git a/netlify/functions/notify-comp-token.js b/netlify/functions/notify-comp-token.js new file mode 100644 index 0000000..d63bf79 --- /dev/null +++ b/netlify/functions/notify-comp-token.js @@ -0,0 +1,96 @@ +const crypto = require('crypto'); + +const headers = { + 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', +}; + +const base64Url = (value) => Buffer.from(value).toString('base64url'); + +const signJwt = (claims, secret) => { + const encodedHeader = base64Url(JSON.stringify({ alg: 'HS256', typ: 'JWT' })); + const encodedPayload = base64Url(JSON.stringify(claims)); + const signature = crypto + .createHmac('sha256', secret) + .update(`${encodedHeader}.${encodedPayload}`) + .digest('base64url'); + + return `${encodedHeader}.${encodedPayload}.${signature}`; +}; + +exports.handler = async (event) => { + if (event.httpMethod === 'OPTIONS') { + return { statusCode: 204, headers }; + } + + if (event.httpMethod !== 'POST') { + return { + statusCode: 405, + headers, + body: JSON.stringify({ message: 'Method not allowed' }), + }; + } + + const secret = process.env.COMPETITION_GROUPS_JWT_SECRET; + if (!secret) { + return { + statusCode: 500, + headers, + body: JSON.stringify({ message: 'Notification token secret is not configured' }), + }; + } + + const { accessToken } = JSON.parse(event.body || '{}'); + if (!accessToken) { + return { + statusCode: 400, + headers, + body: JSON.stringify({ message: 'Missing WCA access token' }), + }; + } + + const wcaOrigin = process.env.WCA_ORIGIN || 'https://www.worldcubeassociation.org'; + const meResponse = await fetch(`${wcaOrigin}/api/v0/me`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + if (!meResponse.ok) { + return { + statusCode: 401, + headers, + body: JSON.stringify({ message: 'Invalid WCA access token' }), + }; + } + + const { me } = await meResponse.json(); + if (!me?.id) { + return { + statusCode: 401, + headers, + body: JSON.stringify({ message: 'Unable to resolve WCA user' }), + }; + } + + const now = Math.floor(Date.now() / 1000); + const token = signJwt( + { + aud: process.env.COMPETITION_GROUPS_JWT_AUDIENCE || 'notifycomp', + exp: now + 10 * 60, + iat: now, + iss: process.env.COMPETITION_GROUPS_JWT_ISSUER || 'competitiongroups.com', + sub: `wca:${me.id}`, + wcaUserIds: [me.id], + }, + secret, + ); + + return { + statusCode: 200, + headers, + body: JSON.stringify({ token }), + }; +}; diff --git a/public/notification-sw.js b/public/notification-sw.js new file mode 100644 index 0000000..e0e0b11 --- /dev/null +++ b/public/notification-sw.js @@ -0,0 +1,34 @@ +self.addEventListener('push', (event) => { + if (!event.data) { + return; + } + + const payload = event.data.json(); + const title = payload.title || 'Assignment update'; + const options = { + body: payload.body, + data: payload, + tag: payload.dedupeKey || 'assignment-change', + }; + + event.waitUntil(self.registration.showNotification(title, options)); +}); + +self.addEventListener('notificationclick', (event) => { + event.notification.close(); + + const targetUrl = event.notification.data?.url || '/settings'; + const url = new URL(targetUrl, self.location.origin).href; + + event.waitUntil( + self.clients.matchAll({ includeUncontrolled: true, type: 'window' }).then((clients) => { + const existingClient = clients.find((client) => client.url === url); + + if (existingClient) { + return existingClient.focus(); + } + + return self.clients.openWindow(url); + }), + ); +}); diff --git a/src/hooks/useAssignmentNotifications/index.ts b/src/hooks/useAssignmentNotifications/index.ts new file mode 100644 index 0000000..203cfca --- /dev/null +++ b/src/hooks/useAssignmentNotifications/index.ts @@ -0,0 +1 @@ +export * from './useAssignmentNotifications'; diff --git a/src/hooks/useAssignmentNotifications/useAssignmentNotifications.ts b/src/hooks/useAssignmentNotifications/useAssignmentNotifications.ts new file mode 100644 index 0000000..9e0b7bb --- /dev/null +++ b/src/hooks/useAssignmentNotifications/useAssignmentNotifications.ts @@ -0,0 +1,74 @@ +import { useCallback, useMemo, useState } from 'react'; +import { + AssignmentNotificationStatus, + disableAssignmentNotifications, + enableAssignmentNotifications, + getAssignmentNotificationStatus, +} from '@/lib/notifications/assignmentNotifications'; + +interface UseAssignmentNotificationsParams { + competitions: ApiCompetition[]; + user: User | null; +} + +export function useAssignmentNotifications({ + competitions, + user, +}: UseAssignmentNotificationsParams) { + const [status, setStatus] = useState( + getAssignmentNotificationStatus, + ); + const [isSaving, setIsSaving] = useState(false); + const [error, setError] = useState(null); + + const watches = useMemo( + () => + user + ? competitions.map((competition) => ({ + competitionId: competition.id, + wcaUserId: user.id, + })) + : [], + [competitions, user], + ); + + const enable = useCallback(async () => { + setIsSaving(true); + setError(null); + + try { + const nextStatus = await enableAssignmentNotifications(watches); + setStatus(nextStatus); + } catch (e) { + setError(e instanceof Error ? e.message : 'Unable to enable assignment notifications.'); + setStatus(getAssignmentNotificationStatus()); + } finally { + setIsSaving(false); + } + }, [watches]); + + const disable = useCallback(async () => { + setIsSaving(true); + setError(null); + + try { + await disableAssignmentNotifications(); + setStatus(getAssignmentNotificationStatus()); + } catch (e) { + setError(e instanceof Error ? e.message : 'Unable to disable assignment notifications.'); + } finally { + setIsSaving(false); + } + }, []); + + return { + canEnable: status === 'default' && watches.length > 0, + canDisable: status === 'granted', + enable, + disable, + error, + isSaving, + status, + watchCount: watches.length, + }; +} diff --git a/src/lib/notifications/assignmentNotifications.ts b/src/lib/notifications/assignmentNotifications.ts new file mode 100644 index 0000000..a5fd3cb --- /dev/null +++ b/src/lib/notifications/assignmentNotifications.ts @@ -0,0 +1,187 @@ +import { getLocalStorage } from '@/lib/localStorage'; + +const NOTIFY_COMP_ORIGIN = import.meta.env.VITE_NOTIFY_COMP_ORIGIN ?? ''; +const NOTIFY_COMP_TOKEN_URL = '/.netlify/functions/notify-comp-token'; + +interface PushSubscriptionJson { + endpoint?: string; + keys?: { + p256dh?: string; + auth?: string; + }; +} + +export interface AssignmentNotificationWatch { + competitionId: string; + wcaUserId: number; +} + +export type AssignmentNotificationStatus = NotificationPermission | 'not-signed-in' | 'unsupported'; + +const notifyCompUrl = (path: string) => `${NOTIFY_COMP_ORIGIN}${path}`; + +const getAccessToken = () => { + const expiresAt = Number(getLocalStorage('expirationTime') ?? 0); + const accessToken = getLocalStorage('accessToken'); + + if (!accessToken || !expiresAt || expiresAt <= Date.now()) { + return null; + } + + return accessToken; +}; + +const toUint8Array = (base64: string) => { + const padding = '='.repeat((4 - (base64.length % 4)) % 4); + const normalized = `${base64}${padding}`.replace(/-/g, '+').replace(/_/g, '/'); + const raw = window.atob(normalized); + const output = new Uint8Array(raw.length); + + for (let index = 0; index < raw.length; index += 1) { + output[index] = raw.charCodeAt(index); + } + + return output; +}; + +export const getAssignmentNotificationStatus = (): AssignmentNotificationStatus => { + if ( + !('Notification' in window) || + !('serviceWorker' in navigator) || + !('PushManager' in window) + ) { + return 'unsupported'; + } + + if (!getAccessToken()) { + return 'not-signed-in'; + } + + return Notification.permission; +}; + +export const requestAssignmentNotificationPermission = async () => { + if (!('Notification' in window)) { + return 'unsupported' as const; + } + + if (Notification.permission === 'granted') { + return 'granted' as const; + } + + return await Notification.requestPermission(); +}; + +const fetchNotifyCompToken = async () => { + const accessToken = getAccessToken(); + if (!accessToken) { + throw new Error('Sign in with WCA to enable assignment notifications.'); + } + + const response = await fetch(NOTIFY_COMP_TOKEN_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ accessToken }), + }); + + if (!response.ok) { + throw new Error(await response.text()); + } + + const payload = (await response.json()) as { token?: string }; + if (!payload.token) { + throw new Error('Notification token response was missing a token.'); + } + + return payload.token; +}; + +const fetchVapidPublicKey = async () => { + const response = await fetch(notifyCompUrl('/v0/external/push/vapid-public-key')); + + if (!response.ok) { + throw new Error(await response.text()); + } + + const payload = (await response.json()) as { publicKey?: string }; + if (!payload.publicKey) { + throw new Error('NotifyComp did not return a VAPID public key.'); + } + + return payload.publicKey; +}; + +const getPushSubscription = async () => { + const registration = await navigator.serviceWorker.ready; + const existing = await registration.pushManager.getSubscription(); + + if (existing) { + return existing; + } + + return await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: toUint8Array(await fetchVapidPublicKey()), + }); +}; + +export const enableAssignmentNotifications = async (watches: AssignmentNotificationWatch[]) => { + const permission = await requestAssignmentNotificationPermission(); + if (permission !== 'granted') { + return permission; + } + + const [token, subscription] = await Promise.all([fetchNotifyCompToken(), getPushSubscription()]); + const payload = subscription.toJSON() as PushSubscriptionJson; + + if (!payload.endpoint || !payload.keys?.p256dh || !payload.keys.auth) { + throw new Error('Browser push subscription is missing required keys.'); + } + + const response = await fetch(notifyCompUrl('/v0/external/push/subscriptions'), { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + endpoint: payload.endpoint, + p256dh: payload.keys.p256dh, + auth: payload.keys.auth, + watches, + }), + }); + + if (!response.ok) { + throw new Error(await response.text()); + } + + return permission; +}; + +export const disableAssignmentNotifications = async () => { + const registration = await navigator.serviceWorker.ready; + const subscription = await registration.pushManager.getSubscription(); + + if (!subscription) { + return; + } + + const token = await fetchNotifyCompToken(); + const payload = subscription.toJSON() as PushSubscriptionJson; + + if (payload.endpoint) { + await fetch(notifyCompUrl('/v0/external/push/subscriptions'), { + method: 'DELETE', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ endpoint: payload.endpoint }), + }); + } + + await subscription.unsubscribe(); +}; diff --git a/src/pages/Settings/index.tsx b/src/pages/Settings/index.tsx index 54e917c..e9e8844 100644 --- a/src/pages/Settings/index.tsx +++ b/src/pages/Settings/index.tsx @@ -1,8 +1,17 @@ -import { Container } from '@/components'; +import { Button, Container } from '@/components'; +import { useMyCompetitionsQuery } from '@/containers/MyCompetitions/MyCompetitions.query'; +import { useAssignmentNotifications } from '@/hooks/useAssignmentNotifications'; +import { useAuth } from '@/providers/AuthProvider'; import { Theme, useUserSettings } from '@/providers/UserSettingsProvider'; export default function Settings() { const { theme, setTheme } = useUserSettings(); + const { user, signIn } = useAuth(); + const { competitions, isLoading } = useMyCompetitionsQuery(user?.id); + const notifications = useAssignmentNotifications({ + competitions, + user, + }); const themeOptions: { value: Theme; label: string; description: string }[] = [ { value: 'light', label: 'Light', description: 'Always use light theme' }, @@ -12,30 +21,94 @@ export default function Settings() { return ( -

Settings

- -
-

Appearance

- -
- - {themeOptions.map((option) => ( -
- setTheme(option.value)} - className="radio mt-1" - /> - +
+

Settings

+ +
+

Appearance

+ +
+ + {themeOptions.map((option) => ( +
+ setTheme(option.value)} + className="radio" + /> + +
+ ))} +
+
+ +
+
+

Assignment notifications

+

+ Get push notifications when your assignments change for upcoming and ongoing + competitions. +

+

+ Watching {notifications.watchCount} competition + {notifications.watchCount === 1 ? '' : 's'}. +

+
+ + {!user && ( + + )} + + {user && notifications.status === 'unsupported' && ( +

This browser does not support push notifications.

+ )} + + {user && notifications.status === 'not-signed-in' && ( +
+

Sign in with WCA again to enable notifications.

+
- ))} + )} + + {user && notifications.canEnable && ( + + )} + + {user && notifications.canDisable && ( + + )} + + {user && notifications.status === 'denied' && ( +

Notifications are blocked in your browser settings.

+ )} + + {notifications.error &&

{notifications.error}

}
diff --git a/src/providers/AuthProvider/AuthProvider.tsx b/src/providers/AuthProvider/AuthProvider.tsx index 6b86372..5343e95 100644 --- a/src/providers/AuthProvider/AuthProvider.tsx +++ b/src/providers/AuthProvider/AuthProvider.tsx @@ -85,6 +85,11 @@ export function AuthProvider({ children }: PropsWithChildren) { fetchMe(accessToken) .then(({ me, ongoing_competitions, upcoming_competitions }) => { + setLocalStorage('accessToken', accessToken); + setLocalStorage( + 'expirationTime', + String(Date.now() + Number(hashParams.get('expires_in') ?? 0) * 1000), + ); setUserAndSave(me); queryClient.setQueryData(['userCompetitions'], { ongoing_competitions, diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 0dd8293..f884b30 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -3,3 +3,7 @@ declare const __GIT_COMMIT__: string; declare const __GIT_TAG__: string; + +interface ImportMetaEnv { + readonly VITE_NOTIFY_COMP_ORIGIN?: string; +} diff --git a/vite.config.ts b/vite.config.ts index db0eb2e..de5c9ca 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -28,6 +28,9 @@ export default defineConfig({ ViteYaml(), VitePWA({ registerType: 'autoUpdate', + workbox: { + importScripts: ['notification-sw.js'], + }, }), ], build: { From a0eee06d37a248bda224b7a8ff0a250261ab6e8f Mon Sep 17 00:00:00 2001 From: Cailyn Sinclair Date: Mon, 11 May 2026 16:52:42 -0700 Subject: [PATCH 2/2] Fix assignment notification enable flow --- src/apolloClient.ts | 4 +- .../useAssignmentNotifications.ts | 17 ++++- .../notifications/assignmentNotifications.ts | 63 +++++++++++++++---- src/pages/Settings/index.tsx | 6 +- src/vite-env.d.ts | 2 + 5 files changed, 73 insertions(+), 19 deletions(-) diff --git a/src/apolloClient.ts b/src/apolloClient.ts index e5bdc47..a8b84e8 100644 --- a/src/apolloClient.ts +++ b/src/apolloClient.ts @@ -4,12 +4,12 @@ import { getMainDefinition } from '@apollo/client/utilities'; import { createClient } from 'graphql-ws'; const httpLink = createHttpLink({ - uri: import.meta.env.VITE_NOTIFYCOMP_API_ORIGIN || 'https://admin.notifycomp.com/graphql', + uri: import.meta.env.VITE_NOTIFYCOMP_API_ORIGIN || 'https://api.notifycomp.com/api/graphql', }); const wsLink = new GraphQLWsLink( createClient({ - url: import.meta.env.VITE_NOTIFYCOMP_WS_ORIGIN || 'wss://admin.notifycomp.com/graphql', + url: import.meta.env.VITE_NOTIFYCOMP_WS_ORIGIN || 'wss://api.notifycomp.com/api/graphql', }), ); diff --git a/src/hooks/useAssignmentNotifications/useAssignmentNotifications.ts b/src/hooks/useAssignmentNotifications/useAssignmentNotifications.ts index 9e0b7bb..a16e9ab 100644 --- a/src/hooks/useAssignmentNotifications/useAssignmentNotifications.ts +++ b/src/hooks/useAssignmentNotifications/useAssignmentNotifications.ts @@ -1,9 +1,10 @@ -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { AssignmentNotificationStatus, disableAssignmentNotifications, enableAssignmentNotifications, getAssignmentNotificationStatus, + isAssignmentNotificationsEnabled, } from '@/lib/notifications/assignmentNotifications'; interface UseAssignmentNotificationsParams { @@ -20,6 +21,12 @@ export function useAssignmentNotifications({ ); const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); + const [isEnabled, setIsEnabled] = useState(isAssignmentNotificationsEnabled); + + useEffect(() => { + setStatus(getAssignmentNotificationStatus()); + setIsEnabled(isAssignmentNotificationsEnabled()); + }, [user]); const watches = useMemo( () => @@ -39,9 +46,11 @@ export function useAssignmentNotifications({ try { const nextStatus = await enableAssignmentNotifications(watches); setStatus(nextStatus); + setIsEnabled(isAssignmentNotificationsEnabled()); } catch (e) { setError(e instanceof Error ? e.message : 'Unable to enable assignment notifications.'); setStatus(getAssignmentNotificationStatus()); + setIsEnabled(isAssignmentNotificationsEnabled()); } finally { setIsSaving(false); } @@ -54,16 +63,18 @@ export function useAssignmentNotifications({ try { await disableAssignmentNotifications(); setStatus(getAssignmentNotificationStatus()); + setIsEnabled(isAssignmentNotificationsEnabled()); } catch (e) { setError(e instanceof Error ? e.message : 'Unable to disable assignment notifications.'); + setIsEnabled(isAssignmentNotificationsEnabled()); } finally { setIsSaving(false); } }, []); return { - canEnable: status === 'default' && watches.length > 0, - canDisable: status === 'granted', + canEnable: (status === 'default' || status === 'granted') && !isEnabled && watches.length > 0, + canDisable: status === 'granted' && isEnabled, enable, disable, error, diff --git a/src/lib/notifications/assignmentNotifications.ts b/src/lib/notifications/assignmentNotifications.ts index a5fd3cb..e23a21e 100644 --- a/src/lib/notifications/assignmentNotifications.ts +++ b/src/lib/notifications/assignmentNotifications.ts @@ -1,7 +1,10 @@ -import { getLocalStorage } from '@/lib/localStorage'; +import { deleteLocalStorage, getLocalStorage, setLocalStorage } from '@/lib/localStorage'; -const NOTIFY_COMP_ORIGIN = import.meta.env.VITE_NOTIFY_COMP_ORIGIN ?? ''; +const NOTIFY_COMP_ORIGIN = + import.meta.env.VITE_NOTIFY_COMP_ORIGIN ?? 'https://api.notifycomp.com/api'; const NOTIFY_COMP_TOKEN_URL = '/.netlify/functions/notify-comp-token'; +const ENABLED_STORAGE_KEY = 'assignmentNotifications.enabled'; +const SERVICE_WORKER_TIMEOUT_MS = 10000; interface PushSubscriptionJson { endpoint?: string; @@ -16,10 +19,13 @@ export interface AssignmentNotificationWatch { wcaUserId: number; } -export type AssignmentNotificationStatus = NotificationPermission | 'not-signed-in' | 'unsupported'; +export type AssignmentNotificationStatus = NotificationPermission | 'reauthorize' | 'unsupported'; const notifyCompUrl = (path: string) => `${NOTIFY_COMP_ORIGIN}${path}`; +export const isAssignmentNotificationsEnabled = () => + getLocalStorage(ENABLED_STORAGE_KEY) === 'true'; + const getAccessToken = () => { const expiresAt = Number(getLocalStorage('expirationTime') ?? 0); const accessToken = getLocalStorage('accessToken'); @@ -54,7 +60,7 @@ export const getAssignmentNotificationStatus = (): AssignmentNotificationStatus } if (!getAccessToken()) { - return 'not-signed-in'; + return 'reauthorize'; } return Notification.permission; @@ -72,10 +78,21 @@ export const requestAssignmentNotificationPermission = async () => { return await Notification.requestPermission(); }; +const readErrorMessage = async (response: Response) => { + const text = await response.text(); + + try { + const payload = JSON.parse(text) as { message?: string }; + return payload.message || text; + } catch { + return text; + } +}; + const fetchNotifyCompToken = async () => { const accessToken = getAccessToken(); if (!accessToken) { - throw new Error('Sign in with WCA to enable assignment notifications.'); + throw new Error('Refresh your WCA authorization to enable assignment notifications.'); } const response = await fetch(NOTIFY_COMP_TOKEN_URL, { @@ -87,7 +104,7 @@ const fetchNotifyCompToken = async () => { }); if (!response.ok) { - throw new Error(await response.text()); + throw new Error(await readErrorMessage(response)); } const payload = (await response.json()) as { token?: string }; @@ -102,7 +119,7 @@ const fetchVapidPublicKey = async () => { const response = await fetch(notifyCompUrl('/v0/external/push/vapid-public-key')); if (!response.ok) { - throw new Error(await response.text()); + throw new Error(await readErrorMessage(response)); } const payload = (await response.json()) as { publicKey?: string }; @@ -113,8 +130,27 @@ const fetchVapidPublicKey = async () => { return payload.publicKey; }; +const withTimeout = async (promise: Promise, message: string) => + await Promise.race([ + promise, + new Promise((_, reject) => { + window.setTimeout(() => reject(new Error(message)), SERVICE_WORKER_TIMEOUT_MS); + }), + ]); + +const getServiceWorkerRegistration = async () => { + if (import.meta.env.DEV) { + return await navigator.serviceWorker.register('/notification-sw.js'); + } + + return await withTimeout( + navigator.serviceWorker.ready, + 'Notification service worker was not ready. Refresh the page and try again.', + ); +}; + const getPushSubscription = async () => { - const registration = await navigator.serviceWorker.ready; + const registration = await getServiceWorkerRegistration(); const existing = await registration.pushManager.getSubscription(); if (existing) { @@ -133,7 +169,8 @@ export const enableAssignmentNotifications = async (watches: AssignmentNotificat return permission; } - const [token, subscription] = await Promise.all([fetchNotifyCompToken(), getPushSubscription()]); + const token = await fetchNotifyCompToken(); + const subscription = await getPushSubscription(); const payload = subscription.toJSON() as PushSubscriptionJson; if (!payload.endpoint || !payload.keys?.p256dh || !payload.keys.auth) { @@ -155,17 +192,20 @@ export const enableAssignmentNotifications = async (watches: AssignmentNotificat }); if (!response.ok) { - throw new Error(await response.text()); + deleteLocalStorage(ENABLED_STORAGE_KEY); + throw new Error(await readErrorMessage(response)); } + setLocalStorage(ENABLED_STORAGE_KEY, 'true'); return permission; }; export const disableAssignmentNotifications = async () => { - const registration = await navigator.serviceWorker.ready; + const registration = await getServiceWorkerRegistration(); const subscription = await registration.pushManager.getSubscription(); if (!subscription) { + deleteLocalStorage(ENABLED_STORAGE_KEY); return; } @@ -184,4 +224,5 @@ export const disableAssignmentNotifications = async () => { } await subscription.unsubscribe(); + deleteLocalStorage(ENABLED_STORAGE_KEY); }; diff --git a/src/pages/Settings/index.tsx b/src/pages/Settings/index.tsx index e9e8844..bed7b32 100644 --- a/src/pages/Settings/index.tsx +++ b/src/pages/Settings/index.tsx @@ -72,11 +72,11 @@ export default function Settings() {

This browser does not support push notifications.

)} - {user && notifications.status === 'not-signed-in' && ( + {user && notifications.status === 'reauthorize' && (
-

Sign in with WCA again to enable notifications.

+

Refresh your WCA authorization to enable notifications.

)} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index f884b30..22776a0 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -6,4 +6,6 @@ declare const __GIT_TAG__: string; interface ImportMetaEnv { readonly VITE_NOTIFY_COMP_ORIGIN?: string; + readonly VITE_NOTIFYCOMP_API_ORIGIN?: string; + readonly VITE_NOTIFYCOMP_WS_ORIGIN?: string; }