Files
Todo/PUSH_NOTIFICATIONS_INTEGRATION_EXAMPLE.md
Paul Bailey 7cda58440f 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.
2025-12-07 01:22:16 +00:00

7.6 KiB

Push Notifications Integration Example

This shows exactly how to add notifications to your existing code.

Step 1: Import the notification helpers

At the top of your action files, add:

import {
  notifyItemAdded,
  notifyItemRemoved,
  notifyItemCompleted,
  notifyListInvitation,
  notifyInvitationAccepted,
  notifyRemovedFromList,
} from '@/lib/notification-helpers';

Step 2: Add notification calls after actions

Example: Adding an item

Before:

export async function createItem(listId: string, data: any) {
  const session = await getServerSession(authOptions);
  if (!session?.user?.id) throw new Error('Unauthorized');

  const item = await prisma.item.create({
    data: {
      ...data,
      listId,
    },
  });

  revalidatePath(`/list/${listId}`);
  return item;
}

After:

export async function createItem(listId: string, data: any) {
  const session = await getServerSession(authOptions);
  if (!session?.user?.id) throw new Error('Unauthorized');

  const item = await prisma.item.create({
    data: {
      ...data,
      listId,
    },
  });

  // 🔔 Add this line to send notifications
  await notifyItemAdded(listId, item.title, session.user.id);

  revalidatePath(`/list/${listId}`);
  return item;
}

Example: Deleting an item

Before:

export async function deleteItem(itemId: string) {
  const session = await getServerSession(authOptions);
  if (!session?.user?.id) throw new Error('Unauthorized');

  await prisma.item.delete({
    where: { id: itemId },
  });

  revalidatePath('/');
}

After:

export async function deleteItem(itemId: string) {
  const session = await getServerSession(authOptions);
  if (!session?.user?.id) throw new Error('Unauthorized');

  // 🔔 Get item details before deleting
  const item = await prisma.item.findUnique({
    where: { id: itemId },
  });

  await prisma.item.delete({
    where: { id: itemId },
  });

  // 🔔 Send notification
  if (item) {
    await notifyItemRemoved(item.listId, item.title, session.user.id);
  }

  revalidatePath('/');
}

Example: Toggling item completion

Before:

export async function toggleItemComplete(itemId: string) {
  const session = await getServerSession(authOptions);
  if (!session?.user?.id) throw new Error('Unauthorized');

  const item = await prisma.item.findUnique({
    where: { id: itemId },
  });

  const updated = await prisma.item.update({
    where: { id: itemId },
    data: { completed: !item?.completed },
  });

  revalidatePath('/');
  return updated;
}

After:

export async function toggleItemComplete(itemId: string) {
  const session = await getServerSession(authOptions);
  if (!session?.user?.id) throw new Error('Unauthorized');

  const item = await prisma.item.findUnique({
    where: { id: itemId },
  });

  const updated = await prisma.item.update({
    where: { id: itemId },
    data: { completed: !item?.completed },
  });

  // 🔔 Notify when item is completed (not uncompleted)
  if (updated.completed) {
    await notifyItemCompleted(updated.listId, updated.title, session.user.id);
  }

  revalidatePath('/');
  return updated;
}

Example: Inviting to a list

Before:

export async function inviteToList(listId: string, email: string, permission: string) {
  const session = await getServerSession(authOptions);
  if (!session?.user?.id) throw new Error('Unauthorized');

  const invitation = await prisma.invitation.create({
    data: {
      listId,
      email,
      permission,
      inviterId: session.user.id,
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
    },
  });

  return invitation;
}

After:

export async function inviteToList(listId: string, email: string, permission: string) {
  const session = await getServerSession(authOptions);
  if (!session?.user?.id) throw new Error('Unauthorized');

  const invitation = await prisma.invitation.create({
    data: {
      listId,
      email,
      permission,
      inviterId: session.user.id,
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
    },
  });

  // 🔔 Notify the invitee
  await notifyListInvitation(invitation.id, email);

  return invitation;
}

Example: Accepting an invitation

Before:

export async function acceptInvitation(invitationId: string) {
  const session = await getServerSession(authOptions);
  if (!session?.user?.id) throw new Error('Unauthorized');

  const invitation = await prisma.invitation.update({
    where: { id: invitationId },
    data: { status: 'accepted' },
  });

  await prisma.collaborator.create({
    data: {
      userId: session.user.id,
      listId: invitation.listId,
      permission: invitation.permission,
    },
  });

  return invitation;
}

After:

export async function acceptInvitation(invitationId: string) {
  const session = await getServerSession(authOptions);
  if (!session?.user?.id) throw new Error('Unauthorized');

  const invitation = await prisma.invitation.update({
    where: { id: invitationId },
    data: { status: 'accepted' },
  });

  await prisma.collaborator.create({
    data: {
      userId: session.user.id,
      listId: invitation.listId,
      permission: invitation.permission,
    },
  });

  // 🔔 Notify the person who sent the invitation
  await notifyInvitationAccepted(invitationId);

  return invitation;
}

Example: Removing a collaborator

Before:

export async function removeCollaborator(listId: string, userId: string) {
  const session = await getServerSession(authOptions);
  if (!session?.user?.id) throw new Error('Unauthorized');

  await prisma.collaborator.deleteMany({
    where: {
      listId,
      userId,
    },
  });

  revalidatePath(`/list/${listId}`);
}

After:

export async function removeCollaborator(listId: string, userId: string) {
  const session = await getServerSession(authOptions);
  if (!session?.user?.id) throw new Error('Unauthorized');

  // 🔔 Get list details before removing
  const list = await prisma.list.findUnique({
    where: { id: listId },
  });

  await prisma.collaborator.deleteMany({
    where: {
      listId,
      userId,
    },
  });

  // 🔔 Notify the removed user
  if (list) {
    await notifyRemovedFromList(userId, list.title, session.user.id);
  }

  revalidatePath(`/list/${listId}`);
}

Step 3: Add NotificationSettings to Profile Page

In app/profile/page.tsx, add the component:

import NotificationSettings from '@/components/NotificationSettings';

export default function ProfilePage() {
  return (
    <div className="container mx-auto p-4 space-y-6">
      <h1 className="text-2xl font-bold">Profile Settings</h1>
      
      {/* Your existing profile components */}
      <ProfileForm />
      <PasswordChangeForm />
      
      {/* 🔔 Add notification settings */}
      <NotificationSettings />
    </div>
  );
}

That's It!

The notification system will:

  • ✅ Automatically handle permission requests
  • ✅ Store subscriptions per user/device
  • ✅ Send notifications to all relevant users
  • ✅ Exclude the user who triggered the action
  • ✅ Clean up expired subscriptions
  • ✅ Work across multiple devices

Optional: Error Handling

If you want to handle notification failures gracefully:

try {
  await notifyItemAdded(listId, item.title, session.user.id);
} catch (error) {
  // Log but don't fail the action
  console.error('Failed to send notification:', error);
}

Notifications are non-critical, so it's fine to let them fail silently without affecting the main action.