feat: add auto-prompt for push notifications on app load

- Create NotificationPrompt component that appears 3 seconds after app loads
- Add to main layout so it shows on all pages (for logged-in users)
- Prompt only shows if permission not yet requested
- Users can dismiss temporarily or permanently
- Add slide-up animation for smooth appearance
- Create simplified SETUP_PUSH_NOTIFICATIONS.md guide

Users will now be automatically prompted to enable notifications
instead of having to go to the profile page.
This commit is contained in:
2025-12-07 01:24:20 +00:00
parent 7cda58440f
commit 3f1abd527e
4 changed files with 204 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
# Setup Push Notifications - Required Steps
## You MUST do these 3 steps for notifications to work:
### Step 1: Install the web-push package
```bash
npm install
```
This installs the `web-push` package that was added to package.json.
### Step 2: Generate VAPID keys
```bash
npm run vapid:generate
```
This will output something like:
```
VAPID_PUBLIC_KEY=BNxxx...
VAPID_PRIVATE_KEY=xxx...
VAPID_SUBJECT=mailto:your-email@example.com
```
**Copy these 3 lines and add them to your `.env` file.**
⚠️ **Important**: Keep the VAPID_PRIVATE_KEY secret! Don't commit it to git.
### Step 3: Update your database
```bash
npx prisma db push
```
This adds the `PushSubscription` table to your database.
### Step 4: Restart your server
```bash
npm run dev
```
## That's it!
Now when users open your app:
- After 3 seconds, they'll see a notification prompt at the bottom-right
- They can click "Enable Notifications" to subscribe
- Or they can dismiss it and enable later from their profile page
## What happens automatically:
✅ Notification prompt appears 3 seconds after app loads
✅ Only shows if user hasn't been asked before
✅ Users can dismiss temporarily ("Later") or permanently ("Don't ask again")
✅ Notifications are sent when:
- Items are added/removed/completed in shared lists
- Users are invited to lists
- Invitations are accepted
- Collaborators are removed
## Testing
1. Open your app in Chrome
2. Wait 3 seconds
3. Click "Enable Notifications" when the prompt appears
4. Open another browser/incognito window
5. Log in as a different user
6. Add an item to a shared list
7. See the notification in the first browser!
## Troubleshooting
**"Push notifications not configured" error**
- Make sure you ran `npm run vapid:generate`
- Make sure you added the keys to `.env`
- Restart your dev server
**Prompt doesn't appear**
- Check browser console for errors
- Make sure you're on localhost or HTTPS
- Clear localStorage and refresh if you dismissed it permanently
**Notifications don't send**
- Check server console for errors
- Verify VAPID keys are correct in `.env`
- Make sure database was updated with `npx prisma db push`

View File

@@ -63,6 +63,17 @@ mark {
} }
} }
@keyframes slide-up {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.animate-slide-in-right { .animate-slide-in-right {
animation: slide-in-right 0.3s ease-out; animation: slide-in-right 0.3s ease-out;
} }
@@ -75,6 +86,10 @@ mark {
animation: scale-in 0.2s ease-out; animation: scale-in 0.2s ease-out;
} }
.animate-slide-up {
animation: slide-up 0.4s ease-out;
}
/* Smooth transitions for interactive elements */ /* Smooth transitions for interactive elements */
.transition-smooth { .transition-smooth {
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;

View File

@@ -2,6 +2,7 @@ import type { Metadata, Viewport } from 'next'
import './globals.css' import './globals.css'
import ServiceWorkerRegistration from '@/components/ServiceWorkerRegistration' import ServiceWorkerRegistration from '@/components/ServiceWorkerRegistration'
import OfflineIndicator from '@/components/OfflineIndicator' import OfflineIndicator from '@/components/OfflineIndicator'
import NotificationPrompt from '@/components/NotificationPrompt'
import { ToastProvider } from '@/components/ToastContainer' import { ToastProvider } from '@/components/ToastContainer'
import ErrorBoundary from '@/components/ErrorBoundary' import ErrorBoundary from '@/components/ErrorBoundary'
@@ -75,6 +76,7 @@ export default function RootLayout({
<ToastProvider> <ToastProvider>
<ServiceWorkerRegistration /> <ServiceWorkerRegistration />
<OfflineIndicator /> <OfflineIndicator />
<NotificationPrompt />
{children} {children}
</ToastProvider> </ToastProvider>
</ErrorBoundary> </ErrorBoundary>

View File

@@ -0,0 +1,104 @@
'use client';
import { useEffect, useState } from 'react';
import { usePushNotifications } from '@/hooks/usePushNotifications';
import { Bell, X } from 'lucide-react';
export default function NotificationPrompt() {
const { permission, isSubscribed, subscribe } = usePushNotifications();
const [showPrompt, setShowPrompt] = useState(false);
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
// Check if we should show the prompt
const shouldShow =
'Notification' in window &&
'serviceWorker' in navigator &&
permission === 'default' && // Not yet asked
!isSubscribed && // Not subscribed
!dismissed && // Not dismissed this session
!localStorage.getItem('notification-prompt-dismissed'); // Not permanently dismissed
if (shouldShow) {
// Show prompt after a short delay (better UX)
const timer = setTimeout(() => {
setShowPrompt(true);
}, 3000); // 3 seconds after app loads
return () => clearTimeout(timer);
}
}, [permission, isSubscribed, dismissed]);
const handleEnable = async () => {
const success = await subscribe();
if (success) {
setShowPrompt(false);
}
};
const handleDismiss = () => {
setShowPrompt(false);
setDismissed(true);
};
const handleDismissForever = () => {
localStorage.setItem('notification-prompt-dismissed', 'true');
setShowPrompt(false);
setDismissed(true);
};
if (!showPrompt) {
return null;
}
return (
<div className="fixed bottom-4 right-4 left-4 sm:left-auto sm:w-96 z-50 animate-slide-up">
<div className="bg-white rounded-lg shadow-2xl border border-gray-200 p-4">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
<Bell className="w-5 h-5 text-blue-600" />
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-semibold text-gray-900 mb-1">
Stay Updated
</h3>
<p className="text-sm text-gray-600 mb-3">
Get notified when items are added, completed, or when you're invited to lists
</p>
<div className="flex gap-2">
<button
onClick={handleEnable}
className="flex-1 px-3 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 transition-colors"
>
Enable Notifications
</button>
<button
onClick={handleDismiss}
className="px-3 py-2 text-gray-600 text-sm font-medium hover:text-gray-900 transition-colors"
>
Later
</button>
</div>
<button
onClick={handleDismissForever}
className="text-xs text-gray-500 hover:text-gray-700 mt-2 underline"
>
Don't ask again
</button>
</div>
<button
onClick={handleDismiss}
className="flex-shrink-0 text-gray-400 hover:text-gray-600 transition-colors"
aria-label="Close"
>
<X className="w-5 h-5" />
</button>
</div>
</div>
</div>
);
}