Skip to content

Commit e262a19

Browse files
Added comments & cleaned up a bit.
1 parent 6f647e9 commit e262a19

File tree

1 file changed

+105
-56
lines changed

1 file changed

+105
-56
lines changed

src/lib/notifications.ts

+105-56
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,37 @@
1+
/**
2+
* Imports for web push, Firebase messaging, Prisma ORM, and type definitions.
3+
*/
14
import webpush, { WebPushError } from 'web-push'
25
import { messaging } from './firebase' // Adjust as needed
3-
import { PrismaClient, Subscription } from '@prisma/client'
6+
import { Subscription } from '@prisma/client'
47
import { JsonValue } from '@prisma/client/runtime/library'
8+
import prisma from '@/lib/db'
59

6-
const prisma = new PrismaClient()
7-
8-
// Interfaces extending the Prisma Subscription type
9-
export interface WebSubscription extends Subscription {
10-
type: 'web'
11-
keys: JsonValue // Represents { auth: string; p256dh: string } as JsonValue
12-
}
13-
14-
// Interface extending the Prisma Subscription type
15-
export interface FcmSubscription extends Subscription {
16-
type: 'fcm'
17-
keys: JsonValue // Represents { token: string } as JsonValue
18-
}
19-
20-
// Union type covering both subscription types
21-
export type SubscriptionRecord = WebSubscription | FcmSubscription
22-
23-
// Notification payload interface
24-
interface NotificationPayload {
25-
title: string
26-
body: string
27-
url: string
28-
icon?: string
29-
badge?: string
30-
}
31-
32-
// Helper function to validate web keys
33-
export function isWebKeys(keys: JsonValue): keys is { auth: string; p256dh: string } {
34-
return (
35-
typeof keys === 'object' &&
36-
keys !== null &&
37-
'auth' in keys &&
38-
'p256dh' in keys &&
39-
typeof (keys as any).auth === 'string' &&
40-
typeof (keys as any).p256dh === 'string'
41-
)
42-
}
43-
44-
// Helper function to validate FCM keys
45-
export function isFcmKeys(keys: JsonValue): keys is { token: string } {
46-
return (
47-
typeof keys === 'object' &&
48-
keys !== null &&
49-
'token' in keys &&
50-
typeof (keys as any).token === 'string'
51-
)
52-
}
53-
54-
// Unified function to send notifications
10+
/**
11+
* Sends a push notification to a subscription using either web push (VAPID) or FCM.
12+
*
13+
* @param subscription - The subscription record, which can be either WebSubscription or FcmSubscription.
14+
* @param notificationPayload - The notification payload containing title, body, url, etc.
15+
*
16+
* If the subscription type is 'web', it will send a web push notification.
17+
* If the subscription type is 'fcm', it will send an FCM notification.
18+
* If the subscription is invalid (e.g., expired or unregistered), it will be removed from the database.
19+
*/
5520
export async function sendNotification(
5621
subscription: SubscriptionRecord,
5722
notificationPayload: NotificationPayload
5823
): Promise<void> {
5924
try {
6025
console.log('Sending notification to subscription:', subscription)
6126

27+
// Handle browser-based web push notifications.
6228
if (subscription.type === 'web') {
29+
// Validate that the subscription keys match the structure required by web push.
6330
if (!isWebKeys(subscription.keys)) {
6431
throw new Error(`Invalid keys for web subscription: ${JSON.stringify(subscription.keys)}`)
6532
}
6633

34+
// Construct and send a web push notification.
6735
await webpush.sendNotification(
6836
{
6937
endpoint: subscription.endpoint,
@@ -80,11 +48,15 @@ export async function sendNotification(
8048
url: notificationPayload.url,
8149
})
8250
)
83-
} else if (subscription.type === 'fcm') {
51+
}
52+
// Handle mobile push notifications using Firebase Cloud Messaging.
53+
else if (subscription.type === 'fcm') {
54+
// Validate that the subscription keys match the structure required by FCM.
8455
if (!isFcmKeys(subscription.keys)) {
8556
throw new Error(`Invalid keys for FCM subscription: ${JSON.stringify(subscription.keys)}`)
8657
}
8758

59+
// Construct the FCM message.
8860
const fcmMessage = {
8961
notification: {
9062
title: notificationPayload.title,
@@ -96,11 +68,17 @@ export async function sendNotification(
9668
token: subscription.keys.token,
9769
}
9870

71+
// Send the FCM message using Firebase.
9972
const response = await messaging.send(fcmMessage)
10073
console.log('FCM response:', response)
10174
}
10275
} catch (error) {
76+
/**
77+
* Handle errors appropriately. Depending on the type of subscription,
78+
* remove invalid or expired subscriptions from the database if necessary.
79+
*/
10380
if (subscription.type === 'web' && error instanceof WebPushError) {
81+
// Status code 410 indicates that the subscription is no longer valid.
10482
if (error.statusCode === 410) {
10583
await prisma.subscription.delete({ where: { id: subscription.id } })
10684
console.log(`Subscription with id ${subscription.id} removed due to expiration.`)
@@ -109,25 +87,96 @@ export async function sendNotification(
10987
`Failed to send notification to subscription id ${subscription.id}:`,
11088
error.statusCode,
11189
error.body
112-
) //error()
90+
)
11391
}
11492
} else if (subscription.type === 'fcm') {
115-
// Handle FCM-specific errors
93+
// These error codes indicate an invalid or unregistered FCM token.
11694
if (
11795
error.code === 'messaging/invalid-registration-token' ||
11896
error.code === 'messaging/registration-token-not-registered'
11997
) {
120-
// Remove the invalid token from the database
12198
await prisma.subscription.delete({ where: { id: subscription.id } })
12299
console.log(`Removed invalid FCM token for subscription id ${subscription.id}.`)
123100
} else {
124-
console.log(`Failed to send FCM notification to subscription id ${subscription.id}:`, error) //error()
101+
console.log(`Failed to send FCM notification to subscription id ${subscription.id}:`, error)
125102
}
126103
} else {
127104
console.log(
128105
`An error occurred while sending notification to subscription id ${subscription.id}:`,
129106
error
130-
) //error()
107+
)
131108
}
132109
}
133110
}
111+
112+
/**
113+
* An interface that extends the Prisma Subscription model for 'web' subscriptions.
114+
* This includes the additional `type` and `keys` fields relevant to web push subscriptions.
115+
*/
116+
export interface WebSubscription extends Subscription {
117+
type: 'web'
118+
// The keys stored as JsonValue actually correspond to { auth: string; p256dh: string }
119+
keys: JsonValue
120+
}
121+
122+
/**
123+
* An interface that extends the Prisma Subscription model for 'fcm' subscriptions.
124+
* This includes the additional `type` and `keys` fields relevant to Firebase Cloud Messaging subscriptions.
125+
*/
126+
export interface FcmSubscription extends Subscription {
127+
type: 'fcm'
128+
// The keys stored as JsonValue actually correspond to { token: string }
129+
keys: JsonValue
130+
}
131+
132+
/**
133+
* A union type that represents either a WebSubscription or an FcmSubscription.
134+
* Used for handling either type of push subscription in a unified way.
135+
*/
136+
export type SubscriptionRecord = WebSubscription | FcmSubscription
137+
138+
/**
139+
* The payload for the notification, including title, body, url, icon, and badge.
140+
* Used for constructing messages for both web push and FCM.
141+
*/
142+
interface NotificationPayload {
143+
title: string
144+
body: string
145+
url: string
146+
icon?: string
147+
badge?: string
148+
}
149+
150+
/**
151+
* Type guard to check if the given JsonValue matches
152+
* the structure required by a web push subscription ({ auth, p256dh }).
153+
*
154+
* @param keys The JsonValue that should contain web push keys.
155+
* @returns True if the object has valid auth and p256dh strings; otherwise, false.
156+
*/
157+
export function isWebKeys(keys: JsonValue): keys is { auth: string; p256dh: string } {
158+
return (
159+
typeof keys === 'object' &&
160+
keys !== null &&
161+
'auth' in keys &&
162+
'p256dh' in keys &&
163+
typeof (keys as any).auth === 'string' &&
164+
typeof (keys as any).p256dh === 'string'
165+
)
166+
}
167+
168+
/**
169+
* Type guard to check if the given JsonValue matches
170+
* the structure required by an FCM subscription ({ token }).
171+
*
172+
* @param keys The JsonValue that should contain the FCM token.
173+
* @returns True if the object has a valid token string; otherwise, false.
174+
*/
175+
export function isFcmKeys(keys: JsonValue): keys is { token: string } {
176+
return (
177+
typeof keys === 'object' &&
178+
keys !== null &&
179+
'token' in keys &&
180+
typeof (keys as any).token === 'string'
181+
)
182+
}

0 commit comments

Comments
 (0)