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

339 lines
7.6 KiB
Markdown

# 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:
```typescript
import {
notifyItemAdded,
notifyItemRemoved,
notifyItemCompleted,
notifyListInvitation,
notifyInvitationAccepted,
notifyRemovedFromList,
} from '@/lib/notification-helpers';
```
## Step 2: Add notification calls after actions
### Example: Adding an item
**Before:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:
```typescript
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:
```typescript
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.