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 {
|
.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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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