docs: add execution plan for fixing push notifications and enhancing PWA experience
This commit is contained in:
865
docs/plans/FixPushNotificationsAndEnhancePWAExperience.md
Normal file
865
docs/plans/FixPushNotificationsAndEnhancePWAExperience.md
Normal file
@@ -0,0 +1,865 @@
|
||||
# Execution Plan: Fix Push Notifications and Enhance PWA Experience
|
||||
|
||||
**OUTCOME_NAME:** FixPushNotificationsAndEnhancePWAExperience
|
||||
|
||||
**Created:** 22 December 2025
|
||||
|
||||
**Problem Statement:** The InstaRecipe PWA has a critical push notification bug causing `InvalidCharacterError` when subscribing to notifications due to improper VAPID key encoding. Additionally, the app lacks an engaging PWA installation prompt to encourage users to install the app, and the notification settings are hidden when the queue is empty, reducing user visibility of this important feature. These issues negatively impact user engagement and the overall PWA experience.
|
||||
|
||||
---
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### Push Notification Bug Analysis
|
||||
|
||||
**Error Details:**
|
||||
```
|
||||
[PushManager] Subscription failed: InvalidCharacterError: Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.
|
||||
at PushNotificationManager.urlBase64ToUint8Array (PushNotificationManager.ts:318:28)
|
||||
at PushNotificationManager.subscribe (PushNotificationManager.ts:193:36)
|
||||
```
|
||||
|
||||
**Root Cause:**
|
||||
- The `urlBase64ToUint8Array` method in `PushNotificationManager.ts` is receiving an invalid base64-encoded VAPID public key
|
||||
- Current development fallback key `'BDummyPublicKeyForDevelopment'` may not be properly formatted
|
||||
- The method lacks validation for malformed base64 strings
|
||||
- URL-safe base64 conversion may be failing due to improper padding or invalid characters
|
||||
|
||||
**Current VAPID Configuration:**
|
||||
- Development keys: `'BDummyPublicKeyForDevelopment'` / `'DummyPrivateKeyForDevelopment'`
|
||||
- Keys are configured in `/src/lib/server/queue/config.ts`
|
||||
- API endpoint `/api/notifications/vapid-key` returns the public key
|
||||
|
||||
### PWA Installation Experience Analysis
|
||||
|
||||
**Current State:**
|
||||
- PWA manifest exists in `static/manifest.json`
|
||||
- Service worker handles installation properly
|
||||
- No proactive installation prompt or encouragement
|
||||
- Users must discover PWA installation through browser UI
|
||||
- Missing engagement opportunity for app adoption
|
||||
|
||||
**Browser Support:**
|
||||
- Chrome/Edge: `beforeinstallprompt` event support
|
||||
- Safari: Manual installation through Share menu
|
||||
- Firefox: Limited PWA support
|
||||
- Android: Full PWA installation support
|
||||
- iOS: Add to Home Screen functionality
|
||||
|
||||
### Notification Settings Visibility
|
||||
|
||||
**Current Implementation:**
|
||||
```svelte
|
||||
<!-- Only shown when queue has items or filters applied -->
|
||||
{#if filteredItems.length > 0 || filter !== 'all'}
|
||||
<div class="mt-8">
|
||||
<NotificationSettings />
|
||||
</div>
|
||||
{/if}
|
||||
```
|
||||
|
||||
**Issues:**
|
||||
- Notification settings hidden when queue is empty
|
||||
- Reduces user awareness of push notification feature
|
||||
- Users may never discover notification functionality
|
||||
- Poor UX for first-time users or when queue is cleared
|
||||
|
||||
---
|
||||
|
||||
## Cross-Reference Check
|
||||
|
||||
### Hidden Dependencies
|
||||
- **Service Worker Integration**: PWA install prompt needs service worker coordination
|
||||
- **Push Notification Service**: Server-side VAPID key validation and generation
|
||||
- **Browser Storage**: Install prompt dismissal state persistence
|
||||
- **Event Handling**: beforeinstallprompt event management across page navigation
|
||||
- **Layout Integration**: Install prompt positioning and responsive design
|
||||
- **Queue Management**: Notification settings should work independently of queue state
|
||||
|
||||
### Side Effects Analysis
|
||||
- **Push Notification Fix**: May require regenerating VAPID keys, affecting existing subscriptions
|
||||
- **Install Prompt**: May impact layout and user flow, requires careful UX design
|
||||
- **Always Show Notifications**: May affect page layout when queue is empty
|
||||
- **Browser Compatibility**: Install prompt behavior varies across browsers and platforms
|
||||
|
||||
---
|
||||
|
||||
## Solution Architecture
|
||||
|
||||
### 1. Push Notification Fix Strategy
|
||||
- **Input Validation**: Add comprehensive validation for VAPID keys
|
||||
- **Error Handling**: Implement graceful degradation for malformed keys
|
||||
- **Key Generation**: Ensure proper URL-safe base64 encoding
|
||||
- **Development Keys**: Generate valid development VAPID key pairs
|
||||
- **Logging**: Enhanced error logging for debugging
|
||||
|
||||
### 2. PWA Install Prompt Design
|
||||
- **Progressive Disclosure**: Show after user engagement, not immediately
|
||||
- **Modern UI**: Attractive slide-up banner with app benefits
|
||||
- **Dismissal Logic**: Remember user dismissal preference
|
||||
- **Cross-Platform**: Handle different installation methods
|
||||
- **Fallback Instructions**: Manual installation guidance when needed
|
||||
|
||||
### 3. Notification Settings Enhancement
|
||||
- **Always Visible**: Remove conditional display logic
|
||||
- **Empty State Design**: Optimize layout for when queue is empty
|
||||
- **User Education**: Better messaging about notification benefits
|
||||
- **Progressive Enhancement**: Works with or without queue items
|
||||
|
||||
---
|
||||
|
||||
## Story Breakdown
|
||||
|
||||
### Story 1: Fix VAPID Key Encoding and Validation
|
||||
|
||||
**Priority:** Critical
|
||||
**Dependencies:** None
|
||||
**Estimated Effort:** 2-3 hours
|
||||
|
||||
**Objective:** Fix the push notification subscription error by implementing proper VAPID key validation and encoding in the client-side push notification manager.
|
||||
|
||||
**Tasks:**
|
||||
1. Add input validation to `urlBase64ToUint8Array` method
|
||||
2. Implement proper error handling for malformed base64 strings
|
||||
3. Generate valid development VAPID key pairs
|
||||
4. Add comprehensive logging for debugging
|
||||
5. Test with both development and production keys
|
||||
6. Update server-side key validation if needed
|
||||
|
||||
**Technical Implementation:**
|
||||
|
||||
**File:** `src/lib/client/PushNotificationManager.ts`
|
||||
```typescript
|
||||
/**
|
||||
* Convert URL-safe base64 string to Uint8Array
|
||||
* Enhanced with validation and error handling
|
||||
*/
|
||||
private urlBase64ToUint8Array(base64String: string): Uint8Array {
|
||||
// Input validation
|
||||
if (!base64String || typeof base64String !== 'string') {
|
||||
console.error('[PushManager] Invalid VAPID key: empty or non-string');
|
||||
return new Uint8Array(0);
|
||||
}
|
||||
|
||||
// Remove whitespace and validate format
|
||||
const cleanKey = base64String.trim();
|
||||
if (cleanKey.length === 0) {
|
||||
console.error('[PushManager] Invalid VAPID key: empty string');
|
||||
return new Uint8Array(0);
|
||||
}
|
||||
|
||||
// VAPID keys should be 65 characters (unpadded base64)
|
||||
if (cleanKey.length !== 65) {
|
||||
console.warn(`[PushManager] VAPID key length ${cleanKey.length}, expected 65`);
|
||||
}
|
||||
|
||||
try {
|
||||
// Add proper padding
|
||||
const padding = '='.repeat((4 - cleanKey.length % 4) % 4);
|
||||
const base64 = (cleanKey + padding)
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/');
|
||||
|
||||
// Validate base64 format before decoding
|
||||
const base64Regex = /^[A-Za-z0-9+\/]*={0,2}$/;
|
||||
if (!base64Regex.test(base64)) {
|
||||
throw new Error('Invalid base64 characters');
|
||||
}
|
||||
|
||||
const rawData = window.atob(base64);
|
||||
const outputArray = new Uint8Array(rawData.length);
|
||||
|
||||
for (let i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i);
|
||||
}
|
||||
|
||||
console.log(`[PushManager] Successfully decoded VAPID key (${outputArray.length} bytes)`);
|
||||
return outputArray;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[PushManager] Failed to decode VAPID key:', error, 'Key:', cleanKey);
|
||||
throw new Error(`Invalid VAPID key format: ${error.message}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**File:** `src/lib/server/queue/config.ts`
|
||||
```typescript
|
||||
// Generate valid development VAPID keys
|
||||
const DEV_VAPID_PUBLIC = 'BEl62iUYgUivyFyKdkfqwZ6d4PzGzZLrm8WQKhQ1m9XYp-b4d4nDwhY-k5tJ-5Yip5S0GYnP-F8i6hPzI-6LrpM';
|
||||
const DEV_VAPID_PRIVATE = 'rGZ-YwUrIX1g1z9GmQdpqYBhZFqLj1Ih0KKYGFCfQ8Y';
|
||||
|
||||
export const queueConfig = {
|
||||
// ... existing config
|
||||
|
||||
/** Web Push notification settings with proper development keys */
|
||||
push: {
|
||||
vapidPublicKey: env.VAPID_PUBLIC_KEY || DEV_VAPID_PUBLIC,
|
||||
vapidPrivateKey: env.VAPID_PRIVATE_KEY || DEV_VAPID_PRIVATE
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- ✅ Push notification subscription succeeds with valid VAPID keys
|
||||
- ✅ Invalid keys are gracefully handled with meaningful error messages
|
||||
- ✅ Development environment has working push notifications
|
||||
- ✅ Production keys are validated and work correctly
|
||||
- ✅ Comprehensive error logging aids debugging
|
||||
- ✅ No breaking changes to existing notification functionality
|
||||
|
||||
**Testing Strategy:**
|
||||
1. Test with various invalid key formats (empty, malformed, wrong length)
|
||||
2. Test with valid development keys
|
||||
3. Test error handling and logging
|
||||
4. Test notification subscription flow end-to-end
|
||||
5. Cross-browser testing for push notification support
|
||||
|
||||
**Files:**
|
||||
- `src/lib/client/PushNotificationManager.ts` (modify)
|
||||
- `src/lib/server/queue/config.ts` (modify)
|
||||
- `src/routes/api/notifications/vapid-key/+server.ts` (review/modify)
|
||||
|
||||
---
|
||||
|
||||
### Story 2: Create Attractive PWA Install Prompt Component
|
||||
|
||||
**Priority:** High
|
||||
**Dependencies:** None
|
||||
**Estimated Effort:** 4-5 hours
|
||||
|
||||
**Objective:** Design and implement an engaging, modern PWA installation prompt that encourages users to install the InstaRecipe app with proper cross-browser support and user experience best practices.
|
||||
|
||||
**Tasks:**
|
||||
1. Create `InstallPrompt.svelte` component with modern UI design
|
||||
2. Implement `beforeinstallprompt` event handling
|
||||
3. Add user engagement detection and timing logic
|
||||
4. Create dismissal state management with localStorage
|
||||
5. Design fallback instructions for different browsers
|
||||
6. Add responsive design for mobile and desktop
|
||||
7. Integrate component into main layout
|
||||
8. Implement analytics for install prompt interactions
|
||||
|
||||
**Technical Implementation:**
|
||||
|
||||
**File:** `src/lib/client/PWAInstallManager.ts`
|
||||
```typescript
|
||||
/**
|
||||
* PWA Installation Manager
|
||||
* Handles beforeinstallprompt event and installation flow
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
interface BeforeInstallPromptEvent extends Event {
|
||||
prompt(): Promise<void>;
|
||||
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
|
||||
}
|
||||
|
||||
export class PWAInstallManager {
|
||||
private deferredPrompt: BeforeInstallPromptEvent | null = null;
|
||||
private listeners: Array<(canInstall: boolean) => void> = [];
|
||||
private installable = false;
|
||||
|
||||
constructor() {
|
||||
if (browser) {
|
||||
this.initializeInstallPrompt();
|
||||
}
|
||||
}
|
||||
|
||||
private initializeInstallPrompt(): void {
|
||||
// Listen for beforeinstallprompt
|
||||
window.addEventListener('beforeinstallprompt', (e: Event) => {
|
||||
e.preventDefault();
|
||||
this.deferredPrompt = e as BeforeInstallPromptEvent;
|
||||
this.installable = true;
|
||||
this.notifyListeners(true);
|
||||
console.log('[PWA] Install prompt available');
|
||||
});
|
||||
|
||||
// Listen for app installation
|
||||
window.addEventListener('appinstalled', () => {
|
||||
console.log('[PWA] App was installed');
|
||||
this.installable = false;
|
||||
this.deferredPrompt = null;
|
||||
this.notifyListeners(false);
|
||||
});
|
||||
}
|
||||
|
||||
public canInstall(): boolean {
|
||||
return this.installable && this.deferredPrompt !== null;
|
||||
}
|
||||
|
||||
public async showInstallPrompt(): Promise<'accepted' | 'dismissed' | 'unavailable'> {
|
||||
if (!this.deferredPrompt) {
|
||||
return 'unavailable';
|
||||
}
|
||||
|
||||
try {
|
||||
await this.deferredPrompt.prompt();
|
||||
const { outcome } = await this.deferredPrompt.userChoice;
|
||||
|
||||
this.deferredPrompt = null;
|
||||
this.installable = false;
|
||||
this.notifyListeners(false);
|
||||
|
||||
console.log(`[PWA] Install prompt ${outcome}`);
|
||||
return outcome;
|
||||
} catch (error) {
|
||||
console.error('[PWA] Install prompt failed:', error);
|
||||
return 'dismissed';
|
||||
}
|
||||
}
|
||||
|
||||
public onInstallStateChange(callback: (canInstall: boolean) => void): () => void {
|
||||
this.listeners.push(callback);
|
||||
return () => {
|
||||
this.listeners = this.listeners.filter(cb => cb !== callback);
|
||||
};
|
||||
}
|
||||
|
||||
private notifyListeners(canInstall: boolean): void {
|
||||
this.listeners.forEach(callback => callback(canInstall));
|
||||
}
|
||||
|
||||
public isStandalone(): boolean {
|
||||
if (!browser) return false;
|
||||
|
||||
return window.matchMedia('(display-mode: standalone)').matches ||
|
||||
(window.navigator as any).standalone ||
|
||||
document.referrer.includes('android-app://');
|
||||
}
|
||||
|
||||
public isDismissed(): boolean {
|
||||
if (!browser) return false;
|
||||
return localStorage.getItem('pwa-install-dismissed') === 'true';
|
||||
}
|
||||
|
||||
public setDismissed(): void {
|
||||
if (browser) {
|
||||
localStorage.setItem('pwa-install-dismissed', 'true');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const pwaInstallManager = new PWAInstallManager();
|
||||
```
|
||||
|
||||
**File:** `src/routes/components/InstallPrompt.svelte`
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { pwaInstallManager } from '$lib/client/PWAInstallManager';
|
||||
|
||||
let showPrompt = $state(false);
|
||||
let canInstall = $state(false);
|
||||
let installing = $state(false);
|
||||
let userEngaged = $state(false);
|
||||
let unsubscribe: (() => void) | null = null;
|
||||
|
||||
onMount(() => {
|
||||
// Don't show if already dismissed or in standalone mode
|
||||
if (pwaInstallManager.isDismissed() || pwaInstallManager.isStandalone()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for install state changes
|
||||
unsubscribe = pwaInstallManager.onInstallStateChange((installable) => {
|
||||
canInstall = installable;
|
||||
|
||||
// Show prompt after user engagement and delay
|
||||
if (installable && userEngaged && !pwaInstallManager.isDismissed()) {
|
||||
setTimeout(() => {
|
||||
showPrompt = true;
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
// Detect user engagement
|
||||
const detectEngagement = () => {
|
||||
userEngaged = true;
|
||||
document.removeEventListener('scroll', detectEngagement);
|
||||
document.removeEventListener('click', detectEngagement);
|
||||
document.removeEventListener('keydown', detectEngagement);
|
||||
};
|
||||
|
||||
document.addEventListener('scroll', detectEngagement, { once: true });
|
||||
document.addEventListener('click', detectEngagement, { once: true });
|
||||
document.addEventListener('keydown', detectEngagement, { once: true });
|
||||
|
||||
return () => {
|
||||
unsubscribe?.();
|
||||
document.removeEventListener('scroll', detectEngagement);
|
||||
document.removeEventListener('click', detectEngagement);
|
||||
document.removeEventListener('keydown', detectEngagement);
|
||||
};
|
||||
});
|
||||
|
||||
async function handleInstall() {
|
||||
installing = true;
|
||||
|
||||
try {
|
||||
const result = await pwaInstallManager.showInstallPrompt();
|
||||
|
||||
if (result === 'accepted') {
|
||||
showPrompt = false;
|
||||
} else if (result === 'dismissed') {
|
||||
handleDismiss();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Install failed:', error);
|
||||
} finally {
|
||||
installing = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDismiss() {
|
||||
showPrompt = false;
|
||||
pwaInstallManager.setDismissed();
|
||||
}
|
||||
|
||||
function getBrowser() {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
if (userAgent.includes('chrome') && !userAgent.includes('edg')) return 'chrome';
|
||||
if (userAgent.includes('safari') && !userAgent.includes('chrome')) return 'safari';
|
||||
if (userAgent.includes('firefox')) return 'firefox';
|
||||
if (userAgent.includes('edg')) return 'edge';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function getInstallInstructions() {
|
||||
const browser = getBrowser();
|
||||
const isMobile = /android|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(navigator.userAgent);
|
||||
|
||||
if (isMobile && browser === 'safari') {
|
||||
return 'Tap the Share button and select "Add to Home Screen"';
|
||||
} else if (browser === 'chrome' || browser === 'edge') {
|
||||
return 'Look for the install button in your browser address bar';
|
||||
} else if (browser === 'firefox') {
|
||||
return 'Firefox has limited PWA support. Try Chrome or Edge for the best experience';
|
||||
}
|
||||
return 'Check your browser menu for "Install App" or "Add to Home Screen" options';
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if showPrompt && canInstall}
|
||||
<div class="fixed bottom-0 left-0 right-0 z-50 transform transition-transform duration-300 ease-out animate-slide-up">
|
||||
<div class="bg-gradient-to-r from-blue-600 to-purple-600 text-white shadow-2xl">
|
||||
<div class="px-4 py-4 sm:px-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- App Icon -->
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-12 h-12 bg-white rounded-xl flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M3 5a2 2 0 012-2h10a2 2 0 012 2v8a2 2 0 01-2 2h-2.22l.123.489.804.804A1 1 0 0113 18H7a1 1 0 01-.707-1.707l.804-.804L7.22 15H5a2 2 0 01-2-2V5zm5.771 7H5V5h10v7H8.771z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-lg font-semibold text-white">Install InstaRecipe</h3>
|
||||
<p class="text-blue-100 text-sm">
|
||||
Get faster access and offline support. Works like a native app!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex items-center space-x-2 ml-4">
|
||||
<button
|
||||
onclick={handleInstall}
|
||||
disabled={installing}
|
||||
class="bg-white text-blue-600 px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-50 transition-colors disabled:opacity-50 flex items-center space-x-2"
|
||||
>
|
||||
{#if installing}
|
||||
<svg class="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span>Installing...</span>
|
||||
{:else}
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<span>Install</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onclick={handleDismiss}
|
||||
class="text-blue-100 hover:text-white p-2 rounded-lg hover:bg-white/10 transition-colors"
|
||||
title="Dismiss"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features -->
|
||||
<div class="mt-3 flex flex-wrap gap-3 text-xs text-blue-100">
|
||||
<div class="flex items-center space-x-1">
|
||||
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span>Offline access</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span>Push notifications</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span>Faster loading</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if !canInstall && !pwaInstallManager.isStandalone() && !pwaInstallManager.isDismissed()}
|
||||
<!-- Fallback instructions for browsers that don't support beforeinstallprompt -->
|
||||
<div class="fixed bottom-4 right-4 max-w-sm bg-white border rounded-lg shadow-lg p-4 z-40">
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-sm font-medium text-gray-900">Install InstaRecipe</h4>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
{getInstallInstructions()}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onclick={() => handleDismiss()}
|
||||
class="text-gray-400 hover:text-gray-500"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
@keyframes slide-up {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slide-up 0.3s ease-out;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- ✅ Install prompt appears after user engagement with attractive design
|
||||
- ✅ `beforeinstallprompt` event handling works in Chrome/Edge
|
||||
- ✅ Dismissal state persists across sessions
|
||||
- ✅ Fallback instructions show for unsupported browsers
|
||||
- ✅ Responsive design works on mobile and desktop
|
||||
- ✅ No prompt shown if already installed or dismissed
|
||||
- ✅ Smooth animations and professional appearance
|
||||
- ✅ Analytics track user interactions with install prompt
|
||||
|
||||
**Testing Strategy:**
|
||||
1. Test on Chrome desktop and mobile with beforeinstallprompt
|
||||
2. Test Safari fallback instructions on iOS
|
||||
3. Test dismissal and persistence logic
|
||||
4. Test responsive design at different screen sizes
|
||||
5. Test user engagement detection timing
|
||||
6. Cross-browser compatibility testing
|
||||
|
||||
**Files:**
|
||||
- `src/lib/client/PWAInstallManager.ts` (create)
|
||||
- `src/routes/components/InstallPrompt.svelte` (create)
|
||||
- `src/routes/+layout.svelte` (modify - integrate component)
|
||||
|
||||
---
|
||||
|
||||
### Story 3: Always Show Notification Settings
|
||||
|
||||
**Priority:** Medium
|
||||
**Dependencies:** None
|
||||
**Estimated Effort:** 1 hour
|
||||
|
||||
**Objective:** Remove the conditional display logic for notification settings so users can always access push notification configuration regardless of queue state.
|
||||
|
||||
**Tasks:**
|
||||
1. Remove conditional logic in `+page.svelte`
|
||||
2. Optimize notification settings layout for empty queue state
|
||||
3. Improve messaging when queue is empty
|
||||
4. Test layout and functionality with and without queue items
|
||||
|
||||
**Technical Implementation:**
|
||||
|
||||
**File:** `src/routes/+page.svelte`
|
||||
```svelte
|
||||
<!-- Remove conditional display and always show notification settings -->
|
||||
|
||||
<!-- Notification Settings - Always visible -->
|
||||
<div class="mt-8">
|
||||
<NotificationSettings />
|
||||
</div>
|
||||
```
|
||||
|
||||
**File:** `src/routes/components/NotificationSettings.svelte`
|
||||
```svelte
|
||||
<!-- Enhanced messaging for empty queue state -->
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
Get notified when your recipe extractions complete, even when InstaRecipe is not open.
|
||||
{#if items.length === 0}
|
||||
Start by adding some Instagram recipe URLs to see notifications in action!
|
||||
{/if}
|
||||
</p>
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- ✅ Notification settings always visible regardless of queue state
|
||||
- ✅ Layout looks good with empty queue
|
||||
- ✅ Messaging adapts appropriately to queue state
|
||||
- ✅ No breaking changes to existing functionality
|
||||
- ✅ Responsive design maintained
|
||||
|
||||
**Testing Strategy:**
|
||||
1. Test with empty queue - settings should be visible
|
||||
2. Test with queue items - settings should still be visible
|
||||
3. Test responsive layout at different screen sizes
|
||||
4. Test notification functionality works in both states
|
||||
|
||||
**Files:**
|
||||
- `src/routes/+page.svelte` (modify)
|
||||
- `src/routes/components/NotificationSettings.svelte` (modify)
|
||||
|
||||
---
|
||||
|
||||
### Story 4: Integration and Cross-Browser Testing
|
||||
|
||||
**Priority:** High
|
||||
**Dependencies:** Stories 1, 2, 3
|
||||
**Estimated Effort:** 3-4 hours
|
||||
|
||||
**Objective:** Integrate all components, ensure cross-browser compatibility, and validate the complete user experience across different devices and browsers.
|
||||
|
||||
**Tasks:**
|
||||
1. Integrate InstallPrompt component into main layout
|
||||
2. Test push notifications across browsers
|
||||
3. Test PWA install flow on different devices
|
||||
4. Validate responsive design and accessibility
|
||||
5. Performance testing and optimization
|
||||
6. User experience testing and refinement
|
||||
|
||||
**Integration Points:**
|
||||
|
||||
**File:** `src/routes/+layout.svelte`
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import InstallPrompt from './components/InstallPrompt.svelte';
|
||||
import './layout.css';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="icon" href={favicon} />
|
||||
</svelte:head>
|
||||
|
||||
{@render children()}
|
||||
|
||||
<!-- PWA Install Prompt -->
|
||||
<InstallPrompt />
|
||||
```
|
||||
|
||||
**Testing Matrix:**
|
||||
- **Chrome Desktop**: beforeinstallprompt + push notifications
|
||||
- **Chrome Mobile**: PWA installation + push notifications
|
||||
- **Safari Desktop**: Fallback instructions + limited notifications
|
||||
- **Safari iOS**: Add to Home Screen + notification permissions
|
||||
- **Firefox**: Fallback instructions + push notifications
|
||||
- **Edge**: beforeinstallprompt + push notifications
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- ✅ All browsers show appropriate install prompts or fallback instructions
|
||||
- ✅ Push notifications work across supported browsers
|
||||
- ✅ PWA installation works on mobile and desktop
|
||||
- ✅ Responsive design works across screen sizes
|
||||
- ✅ Performance impact is minimal
|
||||
- ✅ Accessibility standards met (WCAG 2.1 AA)
|
||||
- ✅ User experience is smooth and intuitive
|
||||
|
||||
**Testing Strategy:**
|
||||
1. Cross-browser manual testing on real devices
|
||||
2. Automated testing for notification functionality
|
||||
3. PWA audit scores validation
|
||||
4. Performance impact measurement
|
||||
5. Accessibility testing with screen readers
|
||||
6. User acceptance testing
|
||||
|
||||
**Files:**
|
||||
- `src/routes/+layout.svelte` (modify)
|
||||
- Various component files (review and refine)
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### Functional Requirements
|
||||
- ✅ Push notification subscriptions succeed without errors
|
||||
- ✅ PWA install prompt appears and functions correctly
|
||||
- ✅ Notification settings always accessible to users
|
||||
- ✅ Cross-browser compatibility maintained
|
||||
- ✅ Mobile-first responsive design works properly
|
||||
|
||||
### User Experience Requirements
|
||||
- ✅ Install prompt timing feels natural and non-intrusive
|
||||
- ✅ Dismissal preferences are respected across sessions
|
||||
- ✅ Error messages are user-friendly and actionable
|
||||
- ✅ Loading states and transitions are smooth
|
||||
- ✅ Accessibility requirements met
|
||||
|
||||
### Technical Requirements
|
||||
- ✅ No breaking changes to existing functionality
|
||||
- ✅ Performance impact minimized (<100ms overhead)
|
||||
- ✅ Code follows project conventions and patterns
|
||||
- ✅ Comprehensive error handling and logging
|
||||
- ✅ Browser compatibility documented
|
||||
|
||||
---
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
### High Risk
|
||||
|
||||
**VAPID Key Changes**
|
||||
- **Risk:** Changing VAPID keys invalidates existing subscriptions
|
||||
- **Impact:** Users lose push notification subscriptions until they re-subscribe
|
||||
- **Mitigation:** Implement graceful migration strategy, detect invalid subscriptions
|
||||
- **Rollback:** Revert to original keys and restore functionality
|
||||
|
||||
**Install Prompt UX**
|
||||
- **Risk:** Install prompt appears too frequently or at wrong times
|
||||
- **Impact:** User annoyance and potential dismissal of PWA installation
|
||||
- **Mitigation:** Careful timing logic and user engagement detection
|
||||
- **Rollback:** Disable prompt component via feature flag
|
||||
|
||||
### Medium Risk
|
||||
|
||||
**Cross-Browser Compatibility**
|
||||
- **Risk:** PWA features work differently across browsers
|
||||
- **Impact:** Inconsistent user experience and confusion
|
||||
- **Mitigation:** Thorough cross-browser testing and progressive enhancement
|
||||
- **Rollback:** Browser-specific feature detection and fallbacks
|
||||
|
||||
**Layout Changes**
|
||||
- **Risk:** Always showing notifications affects page layout negatively
|
||||
- **Impact:** Poor user experience when queue is empty
|
||||
- **Mitigation:** Careful design consideration and responsive testing
|
||||
- **Rollback:** Restore conditional display logic
|
||||
|
||||
### Low Risk
|
||||
|
||||
**Performance Impact**
|
||||
- **Risk:** New components add overhead
|
||||
- **Impact:** Slightly slower page loads
|
||||
- **Mitigation:** Lazy loading and performance monitoring
|
||||
- **Rollback:** Remove or optimize heavy components
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Testing
|
||||
- VAPID key validation logic
|
||||
- PWA install manager functionality
|
||||
- Error handling scenarios
|
||||
- Base64 encoding/decoding
|
||||
|
||||
### Integration Testing
|
||||
- Push notification subscription flow
|
||||
- PWA installation process
|
||||
- Component integration
|
||||
- Browser API compatibility
|
||||
|
||||
### Cross-Browser Testing
|
||||
- Chrome (desktop/mobile)
|
||||
- Safari (desktop/iOS)
|
||||
- Firefox (desktop/mobile)
|
||||
- Edge (desktop/mobile)
|
||||
|
||||
### User Acceptance Testing
|
||||
- Install prompt timing and UX
|
||||
- Notification settings accessibility
|
||||
- PWA installation experience
|
||||
- Error recovery scenarios
|
||||
|
||||
---
|
||||
|
||||
## Deployment Considerations
|
||||
|
||||
### Environment Preparation
|
||||
- Generate production VAPID keys if needed
|
||||
- Configure environment variables
|
||||
- Test in staging environment
|
||||
- Backup current notification subscriptions
|
||||
|
||||
### Feature Flags
|
||||
- PWA install prompt can be disabled via environment variable
|
||||
- Notification fixes can be rolled back independently
|
||||
- Progressive rollout capability for install prompt
|
||||
|
||||
### Monitoring
|
||||
- Track install prompt acceptance/dismissal rates
|
||||
- Monitor push notification subscription errors
|
||||
- Track PWA installation completions
|
||||
- Performance monitoring for new components
|
||||
|
||||
### Documentation Updates
|
||||
- Update README with PWA installation instructions
|
||||
- Document VAPID key generation process
|
||||
- Update troubleshooting guide for notifications
|
||||
- Browser compatibility documentation
|
||||
|
||||
---
|
||||
|
||||
## Blast Radius Summary
|
||||
|
||||
**Affected Modules:**
|
||||
- **Push Notification System**: Core fix affects all notification functionality
|
||||
- **PWA Installation**: New component affects main layout and user flow
|
||||
- **Homepage Layout**: Notification settings always visible changes UX
|
||||
- **Browser Compatibility**: Changes affect cross-browser behavior
|
||||
- **Local Storage**: Install prompt dismissal state management
|
||||
- **Service Worker**: PWA installation coordination
|
||||
|
||||
**Hidden Dependencies:**
|
||||
- Existing push notification subscriptions may need re-validation
|
||||
- Service worker caching may need updates for new components
|
||||
- Analytics tracking for install prompt interactions
|
||||
- Environment variable configuration for VAPID keys
|
||||
- User preference storage for install prompt dismissal
|
||||
- Cross-page navigation state management for PWA install manager
|
||||
|
||||
**Side Effects:**
|
||||
- Users with invalid VAPID subscriptions will need to re-subscribe
|
||||
- Install prompt may appear for existing users who haven't dismissed it
|
||||
- Notification settings visibility affects first-time user onboarding
|
||||
- PWA installation may change how users access the application
|
||||
- Additional browser permissions may be requested for notifications
|
||||
- Local storage usage increases with dismissal state management
|
||||
Reference in New Issue
Block a user