feat: implement complete web push notification system
- Add self-hosted push notifications using VAPID (no external services) - Create PushSubscription model in database - Implement server-side push logic with web-push library - Add notification helpers for list events (items, invitations, collaborators) - Create NotificationSettings UI component for profile page - Add usePushNotifications React hook for subscription management - Create API routes for subscription management and VAPID key - Update service worker with push event handlers - Integrate notifications into item actions (add, remove, complete) - Integrate notifications into sharing actions (invite, accept, remove) - Add VAPID key generation script - Add comprehensive documentation and setup guides Notifications are sent when: - Items are added/removed/completed in shared lists - Users are invited to lists - Invitations are accepted - Collaborators are removed from lists Works on iOS 16.4+ (when installed to home screen), Android, and desktop browsers.
This commit is contained in:
@@ -6,6 +6,11 @@ import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/pages/api/auth/[...nextauth]'
|
||||
import { z } from 'zod'
|
||||
import { checkRateLimit, INVITATION_RATE_LIMIT } from './rate-limit'
|
||||
import {
|
||||
notifyListInvitation,
|
||||
notifyInvitationAccepted,
|
||||
notifyRemovedFromList,
|
||||
} from './notification-helpers'
|
||||
|
||||
// Validation schemas
|
||||
const inviteSchema = z.object({
|
||||
@@ -163,6 +168,14 @@ export async function inviteUserToList(
|
||||
})
|
||||
)
|
||||
|
||||
// Send notification to the invitee
|
||||
try {
|
||||
await notifyListInvitation(invitation.id, validEmail)
|
||||
} catch (error) {
|
||||
console.error('Failed to send invitation notification:', error)
|
||||
// Don't fail the action if notification fails
|
||||
}
|
||||
|
||||
revalidatePath(`/list/${listId}`)
|
||||
return invitation
|
||||
} catch (error) {
|
||||
@@ -320,6 +333,14 @@ export async function acceptInvitation(invitationId: string) {
|
||||
})
|
||||
)
|
||||
|
||||
// Notify the person who sent the invitation
|
||||
try {
|
||||
await notifyInvitationAccepted(invitationId)
|
||||
} catch (error) {
|
||||
console.error('Failed to send acceptance notification:', error)
|
||||
// Don't fail the action if notification fails
|
||||
}
|
||||
|
||||
revalidatePath('/')
|
||||
revalidatePath(`/list/${invitation.listId}`)
|
||||
return result
|
||||
@@ -592,6 +613,20 @@ export async function removeCollaborator(
|
||||
throw new Error('List not found or you do not have permission to remove collaborators')
|
||||
}
|
||||
|
||||
// Get collaborator details before deleting
|
||||
const collaborator = await retryOperation(() =>
|
||||
prisma.collaborator.findUnique({
|
||||
where: { id: collaboratorId },
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
// Delete the collaborator
|
||||
await retryOperation(() =>
|
||||
prisma.collaborator.delete({
|
||||
@@ -602,6 +637,16 @@ export async function removeCollaborator(
|
||||
})
|
||||
)
|
||||
|
||||
// Notify the removed user
|
||||
if (collaborator) {
|
||||
try {
|
||||
await notifyRemovedFromList(collaborator.user.id, list.title, userId)
|
||||
} catch (error) {
|
||||
console.error('Failed to send removal notification:', error)
|
||||
// Don't fail the action if notification fails
|
||||
}
|
||||
}
|
||||
|
||||
revalidatePath(`/list/${listId}`)
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user