11import { deleteLocalStorage , getLocalStorage , setLocalStorage } from '@/lib/localStorage' ;
22import { getStoredWcaAccessToken } from '@/lib/wcaAccessToken' ;
3+ import {
4+ clearNotifyCompPushSessionToken ,
5+ getNotifyCompPushSessionToken ,
6+ setNotifyCompPushSessionToken ,
7+ } from './notifyCompPushSession' ;
38
49const NOTIFY_COMP_ORIGIN =
510 import . meta. env . VITE_NOTIFY_COMP_ORIGIN ?? 'https://api.notifycomp.com/api' ;
@@ -15,6 +20,17 @@ interface PushSubscriptionJson {
1520 } ;
1621}
1722
23+ interface PushSessionResponse {
24+ sessionToken ?: string ;
25+ token ?: string ;
26+ }
27+
28+ interface PushSubscriptionPayload {
29+ endpoint : string ;
30+ p256dh : string ;
31+ auth : string ;
32+ }
33+
1834export interface AssignmentNotificationWatch {
1935 competitionId : string ;
2036 wcaUserId : number ;
@@ -25,7 +41,7 @@ export type AssignmentNotificationStatus = NotificationPermission | 'reauthorize
2541const notifyCompUrl = ( path : string ) => `${ NOTIFY_COMP_ORIGIN } ${ path } ` ;
2642
2743export const isAssignmentNotificationsEnabled = ( ) =>
28- getLocalStorage ( ENABLED_STORAGE_KEY ) === 'true' ;
44+ Boolean ( getNotifyCompPushSessionToken ( ) ) || getLocalStorage ( ENABLED_STORAGE_KEY ) === 'true' ;
2945
3046const toUint8Array = ( base64 : string ) => {
3147 const padding = '=' . repeat ( ( 4 - ( base64 . length % 4 ) ) % 4 ) ;
@@ -49,7 +65,7 @@ export const getAssignmentNotificationStatus = (): AssignmentNotificationStatus
4965 return 'unsupported' ;
5066 }
5167
52- if ( ! getStoredWcaAccessToken ( ) ) {
68+ if ( ! getNotifyCompPushSessionToken ( ) && ! getStoredWcaAccessToken ( ) ) {
5369 return 'reauthorize' ;
5470 }
5571
@@ -105,6 +121,9 @@ const fetchNotifyCompToken = async () => {
105121 return payload . token ;
106122} ;
107123
124+ const canFallbackToLegacySubscriptions = ( response : Response ) =>
125+ response . status === 404 || response . status === 405 ;
126+
108127const fetchVapidPublicKey = async ( ) => {
109128 const response = await fetch ( notifyCompUrl ( '/v0/external/push/vapid-public-key' ) ) ;
110129
@@ -153,30 +172,33 @@ const getPushSubscription = async () => {
153172 } ) ;
154173} ;
155174
156- export const enableAssignmentNotifications = async ( watches : AssignmentNotificationWatch [ ] ) => {
157- const permission = await requestAssignmentNotificationPermission ( ) ;
158- if ( permission !== 'granted' ) {
159- return permission ;
160- }
161-
162- const token = await fetchNotifyCompToken ( ) ;
163- const subscription = await getPushSubscription ( ) ;
175+ const pushSubscriptionPayload = ( subscription : PushSubscription ) : PushSubscriptionPayload => {
164176 const payload = subscription . toJSON ( ) as PushSubscriptionJson ;
165177
166178 if ( ! payload . endpoint || ! payload . keys ?. p256dh || ! payload . keys . auth ) {
167179 throw new Error ( 'Browser push subscription is missing required keys.' ) ;
168180 }
169181
182+ return {
183+ auth : payload . keys . auth ,
184+ endpoint : payload . endpoint ,
185+ p256dh : payload . keys . p256dh ,
186+ } ;
187+ } ;
188+
189+ const registerLegacySubscription = async (
190+ authToken : string ,
191+ payload : PushSubscriptionPayload ,
192+ watches : AssignmentNotificationWatch [ ] ,
193+ ) => {
170194 const response = await fetch ( notifyCompUrl ( '/v0/external/push/subscriptions' ) , {
171195 method : 'POST' ,
172196 headers : {
173- Authorization : `Bearer ${ token } ` ,
197+ Authorization : `Bearer ${ authToken } ` ,
174198 'Content-Type' : 'application/json' ,
175199 } ,
176200 body : JSON . stringify ( {
177- endpoint : payload . endpoint ,
178- p256dh : payload . keys . p256dh ,
179- auth : payload . keys . auth ,
201+ ...payload ,
180202 watches,
181203 } ) ,
182204 } ) ;
@@ -185,34 +207,150 @@ export const enableAssignmentNotifications = async (watches: AssignmentNotificat
185207 deleteLocalStorage ( ENABLED_STORAGE_KEY ) ;
186208 throw new Error ( await readErrorMessage ( response ) ) ;
187209 }
210+ } ;
211+
212+ const createPushSession = async (
213+ authToken : string ,
214+ payload : PushSubscriptionPayload ,
215+ watches : AssignmentNotificationWatch [ ] ,
216+ ) => {
217+ const response = await fetch ( notifyCompUrl ( '/v0/external/push/sessions' ) , {
218+ method : 'POST' ,
219+ headers : {
220+ Authorization : `Bearer ${ authToken } ` ,
221+ 'Content-Type' : 'application/json' ,
222+ } ,
223+ body : JSON . stringify ( {
224+ ...payload ,
225+ watches,
226+ } ) ,
227+ } ) ;
228+
229+ if ( canFallbackToLegacySubscriptions ( response ) ) {
230+ await registerLegacySubscription ( authToken , payload , watches ) ;
231+ return ;
232+ }
233+
234+ if ( ! response . ok ) {
235+ clearNotifyCompPushSessionToken ( ) ;
236+ deleteLocalStorage ( ENABLED_STORAGE_KEY ) ;
237+ throw new Error ( await readErrorMessage ( response ) ) ;
238+ }
239+
240+ const session = ( await response . json ( ) ) as PushSessionResponse ;
241+ const sessionToken = session . sessionToken || session . token ;
242+ if ( ! sessionToken ) {
243+ clearNotifyCompPushSessionToken ( ) ;
244+ deleteLocalStorage ( ENABLED_STORAGE_KEY ) ;
245+ throw new Error ( 'NotifyComp push session response was missing a session token.' ) ;
246+ }
247+
248+ setNotifyCompPushSessionToken ( sessionToken ) ;
249+ } ;
250+
251+ const updatePushSession = async (
252+ sessionToken : string ,
253+ payload : PushSubscriptionPayload ,
254+ watches : AssignmentNotificationWatch [ ] ,
255+ ) => {
256+ const response = await fetch ( notifyCompUrl ( '/v0/external/push/sessions/current' ) , {
257+ method : 'PUT' ,
258+ headers : {
259+ Authorization : `Bearer ${ sessionToken } ` ,
260+ 'Content-Type' : 'application/json' ,
261+ } ,
262+ body : JSON . stringify ( {
263+ ...payload ,
264+ watches,
265+ } ) ,
266+ } ) ;
267+
268+ if ( response . status === 401 || response . status === 403 ) {
269+ clearNotifyCompPushSessionToken ( ) ;
270+ return false ;
271+ }
272+
273+ if ( ! response . ok ) {
274+ throw new Error ( await readErrorMessage ( response ) ) ;
275+ }
276+
277+ return true ;
278+ } ;
279+
280+ export const enableAssignmentNotifications = async ( watches : AssignmentNotificationWatch [ ] ) => {
281+ const permission = await requestAssignmentNotificationPermission ( ) ;
282+ if ( permission !== 'granted' ) {
283+ return permission ;
284+ }
285+
286+ const subscription = await getPushSubscription ( ) ;
287+ const payload = pushSubscriptionPayload ( subscription ) ;
288+ const sessionToken = getNotifyCompPushSessionToken ( ) ;
289+
290+ if ( sessionToken && ( await updatePushSession ( sessionToken , payload , watches ) ) ) {
291+ setLocalStorage ( ENABLED_STORAGE_KEY , 'true' ) ;
292+ return permission ;
293+ }
294+
295+ await createPushSession ( await fetchNotifyCompToken ( ) , payload , watches ) ;
188296
189297 setLocalStorage ( ENABLED_STORAGE_KEY , 'true' ) ;
190298 return permission ;
191299} ;
192300
301+ const deletePushSession = async ( sessionToken : string , endpoint ?: string ) => {
302+ const response = await fetch ( notifyCompUrl ( '/v0/external/push/sessions/current' ) , {
303+ method : 'DELETE' ,
304+ headers : {
305+ Authorization : `Bearer ${ sessionToken } ` ,
306+ 'Content-Type' : 'application/json' ,
307+ } ,
308+ body : JSON . stringify ( { endpoint } ) ,
309+ } ) ;
310+
311+ if (
312+ ! response . ok &&
313+ response . status !== 401 &&
314+ response . status !== 403 &&
315+ response . status !== 404
316+ ) {
317+ throw new Error ( await readErrorMessage ( response ) ) ;
318+ }
319+ } ;
320+
321+ const deleteLegacySubscription = async ( endpoint : string ) => {
322+ const token = await fetchNotifyCompToken ( ) ;
323+
324+ await fetch ( notifyCompUrl ( '/v0/external/push/subscriptions' ) , {
325+ method : 'DELETE' ,
326+ headers : {
327+ Authorization : `Bearer ${ token } ` ,
328+ 'Content-Type' : 'application/json' ,
329+ } ,
330+ body : JSON . stringify ( { endpoint } ) ,
331+ } ) ;
332+ } ;
333+
193334export const disableAssignmentNotifications = async ( ) => {
194335 const registration = await getServiceWorkerRegistration ( ) ;
195336 const subscription = await registration . pushManager . getSubscription ( ) ;
337+ const sessionToken = getNotifyCompPushSessionToken ( ) ;
196338
197339 if ( ! subscription ) {
340+ clearNotifyCompPushSessionToken ( ) ;
198341 deleteLocalStorage ( ENABLED_STORAGE_KEY ) ;
199342 return ;
200343 }
201344
202- const token = await fetchNotifyCompToken ( ) ;
203345 const payload = subscription . toJSON ( ) as PushSubscriptionJson ;
204346
205- if ( payload . endpoint ) {
206- await fetch ( notifyCompUrl ( '/v0/external/push/subscriptions' ) , {
207- method : 'DELETE' ,
208- headers : {
209- Authorization : `Bearer ${ token } ` ,
210- 'Content-Type' : 'application/json' ,
211- } ,
212- body : JSON . stringify ( { endpoint : payload . endpoint } ) ,
213- } ) ;
347+ if ( sessionToken ) {
348+ await deletePushSession ( sessionToken , payload . endpoint ) ;
349+ } else if ( payload . endpoint ) {
350+ await deleteLegacySubscription ( payload . endpoint ) ;
214351 }
215352
216353 await subscription . unsubscribe ( ) ;
354+ clearNotifyCompPushSessionToken ( ) ;
217355 deleteLocalStorage ( ENABLED_STORAGE_KEY ) ;
218356} ;
0 commit comments