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' ;
45const NOTIFY_COMP_TOKEN_URL = '/.netlify/functions/notify-comp-token' ;
6+ const ENABLED_STORAGE_KEY = 'assignmentNotifications.enabled' ;
7+ const SERVICE_WORKER_TIMEOUT_MS = 10000 ;
58
69interface 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
2124const notifyCompUrl = ( path : string ) => `${ NOTIFY_COMP_ORIGIN } ${ path } ` ;
2225
26+ export const isAssignmentNotificationsEnabled = ( ) =>
27+ getLocalStorage ( ENABLED_STORAGE_KEY ) === 'true' ;
28+
2329const 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+
7592const 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+
116152const 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
164203export 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} ;
0 commit comments