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:
83
SETUP_PUSH_NOTIFICATIONS.md
Normal file
83
SETUP_PUSH_NOTIFICATIONS.md
Normal 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`
|
||||
@@ -63,6 +63,17 @@ mark {
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-up {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-slide-in-right {
|
||||
animation: slide-in-right 0.3s ease-out;
|
||||
}
|
||||
@@ -75,6 +86,10 @@ mark {
|
||||
animation: scale-in 0.2s ease-out;
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slide-up 0.4s ease-out;
|
||||
}
|
||||
|
||||
/* Smooth transitions for interactive elements */
|
||||
.transition-smooth {
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Metadata, Viewport } from 'next'
|
||||
import './globals.css'
|
||||
import ServiceWorkerRegistration from '@/components/ServiceWorkerRegistration'
|
||||
import OfflineIndicator from '@/components/OfflineIndicator'
|
||||
import NotificationPrompt from '@/components/NotificationPrompt'
|
||||
import { ToastProvider } from '@/components/ToastContainer'
|
||||
import ErrorBoundary from '@/components/ErrorBoundary'
|
||||
|
||||
@@ -75,6 +76,7 @@ export default function RootLayout({
|
||||
<ToastProvider>
|
||||
<ServiceWorkerRegistration />
|
||||
<OfflineIndicator />
|
||||
<NotificationPrompt />
|
||||
{children}
|
||||
</ToastProvider>
|
||||
</ErrorBoundary>
|
||||
|
||||
104
components/NotificationPrompt.tsx
Normal file
104
components/NotificationPrompt.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user