Files
brachnha-insight/_bmad-output/implementation-artifacts/3-4-pwa-install-prompt-manifest.md
Max e9e6fadb1d fix: ChatBubble crash and DeepSeek API compatibility
- Fix ChatBubble to handle non-string content with String() wrapper
- Fix API route to use generateText for non-streaming requests
- Add @ai-sdk/openai-compatible for non-OpenAI providers (DeepSeek, etc.)
- Use Chat Completions API instead of Responses API for compatible providers
- Update ChatBubble tests and fix component exports to kebab-case
- Remove stale PascalCase ChatBubble.tsx file
2026-01-26 16:55:05 +07:00

31 KiB

Story 3.4: PWA Install Prompt & Manifest

Status: done

Story

As a user, I want to install the app to my home screen, So that it feels like a native app.

Acceptance Criteria

  1. Valid Web App Manifest

    • Given the user visits the web app
    • When the browser parses the site
    • Then it finds a valid manifest.json (or generated via manifest.ts) with correct icons, name ("Test01"), and display: standalone settings
  2. Custom Install UI on Engagement

    • Given the user has engaged with the app (e.g., completed 1 session)
    • When the browser supports it (beforeinstallprompt event)
    • Then a custom "Install App" UI element appears (non-intrusive)
    • And clicking it triggers the native install prompt
  3. Standalone Mode Verification

    • Given the app is installed
    • When it launches from Home Screen
    • Then it opens without the browser URL bar (Standalone mode)

Tasks / Subtasks

  • Create/Generate Web App Manifest

    • Create public/manifest.json OR src/app/manifest.ts (Next.js 16+ convention)
    • Configure app name: "Test01"
    • Configure short_name: "Test01"
    • Set display: "standalone"
    • Set orientation: "portrait" (for mobile)
    • Set background_color and theme_color (Morning Mist palette)
    • Configure start_url: "/" (or appropriate entry point)
    • Add icon references (512x512 and 192x192 minimum)
  • Create PWA Icon Assets

    • Create or generate app icons in required sizes:
      • 192x192 (android adaptive icon)
      • 512x512 (standard icon)
      • Optional: maskable icon for safe area insets
    • Place icons in public/icons/ directory
    • Ensure icons match "Morning Mist" theme branding
  • Configure Next.js for PWA

    • Update next.config.ts to enable PWA metadata
    • Add manifest reference to app layout metadata
    • Add theme-color meta tag to layout
    • Add apple-touch-icon link (for iOS fallback)
  • Create InstallPrompt Store (Zustand)

    • Create src/lib/store/install-prompt-store.ts
    • State:
      • isInstallable: boolean (whether beforeinstallprompt fired)
      • isInstalled: boolean (app running in standalone mode)
      • deferredPrompt: BeforeInstallPromptEvent | null (saved event)
    • Actions:
      • setDeferredPrompt(event) - save the beforeinstallprompt event
      • promptInstall() - trigger the saved prompt
      • dismissInstall() - clear the deferred prompt
    • Use atomic selectors for performance
  • Create InstallPrompt Service

    • Create src/services/install-prompt-service.ts
    • Implement initializeInstallPrompt() method
    • Add beforeinstallprompt event listener to window
    • Prevent default browser install prompt
    • Save event to InstallPromptStore
    • Implement checkIfInstalled() - detects standalone mode via window.matchMedia
  • Create InstallPromptButton Component

    • Create src/components/features/pwa/InstallPromptButton.tsx
    • Show button only when isInstallable AND not isInstalled
    • Button style: Non-intrusive, matches Morning Mist theme
    • Position: Fixed bottom-right or in navigation
    • Icon: Download/Install icon (Lucide)
    • On click: call InstallPromptService.promptInstall()
  • Create Engagement Tracker (for showing prompt)

    • Create src/services/engagement-tracker.ts
    • Track session completions in IndexedDB
    • Return whether user has engaged (completed 1+ sessions)
    • Used to conditionally show InstallPromptButton
  • Initialize Install Prompt in App Layout

    • Modify src/app/layout.tsx
    • Initialize InstallPromptService on mount
    • Check standalone mode on mount
    • Render InstallPromptButton conditionally
  • Test PWA Install Flow End-to-End

    • Unit test: InstallPromptStore state management
    • Unit test: InstallPromptService event handling
    • Unit test: checkIfInstalled() returns correct status
    • Integration test: beforeinstallprompt event saved to store
    • Integration test: promptInstall() triggers native prompt
    • Manual test: Install on Chrome Desktop
    • Manual test: Install on Chrome Android
    • Manual test: Verify standalone mode launches correctly
    • Manual test: iOS fallback (Add to Home Screen instructions)

Senior Developer Review (AI)

Reviewer: Max on 2026-01-23

Summary: Automatic code review identified critical issues with the initial implementation, specifically regarding Server-Side Rendering compatibility in layout.tsx. These have been addressed by moving initialization logic to a client component. Configuration gaps were also filled.

Findings & Fixes:

  1. CRITICAL: InstallPromptService initialization logic was in src/app/layout.tsx (Server Component), which would fail at runtime.
    • Fix: Created src/components/features/pwa/PWAInitializer.tsx (Client Component) to handle all client-side service initialization.
    • Fix: Updated src/app/layout.tsx to import and usage PWAInitializer.
  2. MEDIUM: next.config.ts was missing required optimization settings.
    • Fix: Added optimizePackageImports: ['lucide-react'].
  3. MEDIUM: InstallPromptButton was bypassing the Service Layer for the prompt action.
    • Fix: Refactored to call InstallPromptService.promptInstall() directly.

Outcome: Approved with automated fixes applied. Use the new PWAInitializer pattern for all future client-side service setups.

Dev Notes

Architecture Compliance (CRITICAL)

Logic Sandwich Pattern - DO NOT VIOLATE:

  • UI Components MUST NOT directly handle beforeinstallprompt event
  • All install prompt logic MUST go through InstallPromptService service layer
  • InstallPromptService manages event storage and prompt triggering
  • Services return plain success/failure, not browser events directly

State Management - Atomic Selectors Required:

// GOOD - Atomic selectors
const isInstallable = useInstallPromptStore(s => s.isInstallable);
const isInstalled = useInstallPromptStore(s => s.isInstalled);

// BAD - Causes unnecessary re-renders
const { isInstallable, isInstalled } = useInstallPromptStore();

Local-First Data Boundary:

  • Install prompt state is transient (browser session)
  • Engagement tracking (session count) is stored in IndexedDB
  • No server sync required for install prompt logic

Architecture Implementation Details

Story Purpose: This story implements PWA installability - enabling users to install the app to their home screen for a native-app experience. The custom install prompt provides better UX than the browser's default prompt, appearing only after the user has engaged with the app.

PWA Install Flow:

User visits app
    ↓
Browser detects manifest.json
    ↓
User engages (completes 1 session)
    ↓
Browser fires beforeinstallprompt event
    ↓
InstallPromptService captures event (prevents default)
    ↓
Store setDeferredPrompt(event)
    ↓
InstallPromptButton appears (non-intrusive)
    ↓
User clicks "Install App" button
    ↓
InstallPromptService.promptInstall() calls event.prompt()
    ↓
User accepts native browser prompt
    ↓
App installed to home screen

Manifest Configuration (Next.js 16+): For Next.js 16, use the built-in manifest generation:

// src/app/manifest.ts
import { MetadataRoute } from 'next';

export default function manifest(): MetadataRoute.Manifest {
  return {
    name: 'Test01',
    short_name: 'Test01',
    description: 'Turn your daily learning struggles into polished content',
    start_url: '/',
    display: 'standalone',
    background_color: '#F8FAFC',
    theme_color: '#64748B',
    orientation: 'portrait',
    icons: [
      {
        src: '/icons/icon-192x192.png',
        sizes: '192x192',
        type: 'image/png',
        purpose: 'any maskable'
      },
      {
        src: '/icons/icon-512x512.png',
        sizes: '512x512',
        type: 'image/png',
        purpose: 'any maskable'
      }
    ]
  };
}

Next.js Config for PWA:

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  // Ensure manifest is properly served
  experimental: {
    optimizePackageImports: ['lucide-react']
  }
};

export default nextConfig;

Layout Metadata (src/app/layout.tsx):

// Add to existing layout metadata
export const metadata: Metadata = {
  manifest: '/manifest.json', // For manifest.ts, Next.js handles this
  themeColor: '#64748B',
  appleMobileWebAppCapable: 'yes',
  appleMobileWebAppStatusBarStyle: 'default',
  // ... existing metadata
};

InstallPromptStore Implementation:

// src/lib/store/install-prompt-store.ts
import { create } from 'zustand';

interface BeforeInstallPromptEvent extends Event {
  prompt: () => Promise<void>;
  userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
}

interface InstallPromptState {
  isInstallable: boolean;
  isInstalled: boolean;
  deferredPrompt: BeforeInstallPromptEvent | null;

  setDeferredPrompt: (event: BeforeInstallPromptEvent | null) => void;
  setInstallable: (installable: boolean) => void;
  setInstalled: (installed: boolean) => void;
  promptInstall: () => Promise<boolean>;
  dismissInstall: () => void;
}

export const useInstallPromptStore = create<InstallPromptState>((set, get) => ({
  isInstallable: false,
  isInstalled: false,
  deferredPrompt: null,

  setDeferredPrompt: (event) => set({ deferredPrompt: event, isInstallable: !!event }),

  setInstallable: (installable) => set({ isInstallable: installable }),

  setInstalled: (installed) => set({ isInstalled: installed }),

  promptInstall: async () => {
    const { deferredPrompt } = get();
    if (!deferredPrompt) return false;

    deferredPrompt.prompt();
    const { outcome } = await deferredPrompt.userChoice;

    if (outcome === 'accepted') {
      set({ isInstalled: true, deferredPrompt: null, isInstallable: false });
    } else {
      set({ deferredPrompt: null, isInstallable: false });
    }

    return outcome === 'accepted';
  },

  dismissInstall: () => set({ deferredPrompt: null, isInstallable: false })
}));

InstallPromptService Implementation:

// src/services/install-prompt-service.ts
import { useInstallPromptStore } from '@/lib/store/install-prompt-store';

type BeforeInstallPromptEvent = Event & {
  prompt: () => Promise<void>;
  userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
};

export class InstallPromptService {
  private static initialized = false;

  static initialize(): void {
    if (this.initialized) return;
    this.initialized = true;

    // Check if already installed (standalone mode)
    this.checkInstalledStatus();

    // Listen for beforeinstallprompt event
    if (typeof window !== 'undefined') {
      window.addEventListener('beforeinstallprompt', (e) => {
        e.preventDefault();
        const event = e as BeforeInstallPromptEvent;
        useInstallPromptStore.getState().setDeferredPrompt(event);
      });

      // Listen for appinstalled event (user accepted install)
      window.addEventListener('appinstalled', () => {
        useInstallPromptStore.getState().setInstalled(true);
      });
    }
  }

  static checkInstalledStatus(): void {
    if (typeof window === 'undefined') return;

    // Check if running in standalone mode
    const isStandalone =
      window.matchMedia('(display-mode: standalone)').matches ||
      (window.navigator as any).standalone === true; // iOS Safari

    useInstallPromptStore.getState().setInstalled(isStandalone);
  }

  static async promptInstall(): Promise<boolean> {
    return await useInstallPromptStore.getState().promptInstall();
  }
}

InstallPromptButton Component:

// src/components/features/pwa/InstallPromptButton.tsx
import { Download } from 'lucide-react';
import { useInstallPromptStore } from '@/lib/store/install-prompt-store';
import { Button } from '@/components/ui/button';
import { useEngagementStore } from '@/lib/store/engagement-store';

export function InstallPromptButton() {
  const isInstallable = useInstallPromptStore(s => s.isInstallable);
  const isInstalled = useInstallPromptStore(s => s.isInstalled);
  const promptInstall = useInstallPromptStore(s => s.promptInstall);
  const sessionCount = useEngagementStore(s => s.completedSessions);

  // Only show if:
  // 1. Browser supports install prompt
  // 2. App is not already installed
  // 3. User has engaged (completed at least 1 session)
  const shouldShow = isInstallable && !isInstalled && sessionCount > 0;

  if (!shouldShow) return null;

  const handleInstall = async () => {
    const accepted = await promptInstall();
    if (accepted) {
      // Show success feedback
      console.log('App installed successfully');
    }
  };

  return (
    <Button
      onClick={handleInstall}
      variant="outline"
      size="sm"
      className="fixed bottom-20 right-4 shadow-lg animate-fade-in"
    >
      <Download className="w-4 h-4 mr-2" />
      Install App
    </Button>
  );
}

EngagementTracker for Session Count:

// src/services/engagement-tracker.ts
import { db } from '@/lib/db';

// For MVP, use existing drafts count as engagement metric
// Post-MVP: create separate engagement tracking table
export class EngagementTracker {
  static async getCompletedSessionCount(): Promise<number> {
    const count = await db.drafts
      .where('status')
      .equals('completed')
      .count();
    return count;
  }

  static hasEngaged(): Promise<boolean> {
    return this.getCompletedSessionCount().then(count => count > 0);
  }
}

Previous Story Intelligence

From Story 3.3 (Offline Sync Queue):

  • OfflineStore Pattern: Network status detection via navigator.onLine
  • Logic Sandwich: Services handle all business logic, UI just displays state
  • Atomic Selectors: All Zustand stores use atomic selectors
  • Key Learning: Initialize services in layout.tsx on app mount

From Story 3.2 (Deletion):

  • Database Schema v3: SyncQueue table added with proper migration
  • Service Layer Pattern: DraftService for all draft operations
  • Key Learning: All data operations go through service layer

From Epic 1 (Chat):

  • Database Foundation: Dexie.js with IndexedDB
  • Zustand Pattern: Separate stores for different concerns
  • Key Learning: Use feature folders for organized components

UX Design Specifications

From UX Design Document:

Install Prompt Pattern:

  • Non-intrusive appearance (not modal, not blocking)
  • Shows only after user engagement (1+ sessions completed)
  • Bottom-right fixed position (out of way but visible)
  • Dismissible (user can ignore without breaking app)

Visual Feedback:

  • Button Style: Outline variant, Morning Mist colors
  • Icon: Download icon from Lucide React
  • Animation: Subtle fade-in when appears
  • Hover: Subtle highlight to indicate interactivity

Accessibility:

  • aria-label="Install Test01 app to home screen" for button
  • Focus visible for keyboard navigation
  • High contrast text (WCAG AA compliant)

iOS Fallback: iOS Safari doesn't support beforeinstallprompt. Show instructions:

"To install: Tap Share, then 'Add to Home Screen'"

This should be shown in a subtle tooltip or help menu item for iOS users.

Testing Requirements

Unit Tests:

  • InstallPromptStore.setDeferredPrompt() sets isInstallable to true
  • InstallPromptStore.setInstalled() updates isInstalled state
  • InstallPromptStore.promptInstall() calls deferredPrompt.prompt()
  • InstallPromptStore.promptInstall() returns true when accepted
  • InstallPromptStore.promptInstall() returns false when dismissed
  • InstallPromptStore.dismissInstall() clears deferredPrompt
  • InstallPromptService.initialize() adds event listeners
  • InstallPromptService.checkInstalledStatus() detects standalone mode
  • EngagementTracker.hasEngaged() returns true when drafts exist

Integration Tests:

  • beforeinstallprompt event updates InstallPromptStore
  • InstallPromptButton appears when isInstallable=true and sessionCount>0
  • InstallPromptButton does NOT appear when isInstalled=true
  • Clicking button triggers promptInstall()
  • Standalone mode detected via window.matchMedia

Manual Tests (Browser Testing):

  • Chrome Desktop: Verify install prompt appears after engagement
  • Chrome Android: Install to home screen, verify standalone mode
  • Edge Desktop: Same as Chrome
  • Safari Desktop: No prompt (beforeinstallprompt not supported)
  • Safari iOS: Verify "Add to Home Screen" instructions shown
  • Firefox Desktop: Verify prompt appears

Lighthouse PWA Audit:

  • All PWA criteria should pass after implementation
  • Installability: PASS
  • Manifest: PASS
  • Service Worker: PASS (from previous story's offline support)
  • HTTPS: PASS (deployment requirement)

Performance Requirements

NFR-02 Compliance (App Load Time):

  • Manifest.json must be < 5KB (small metadata)
  • Icon assets should be optimized (lossless compression)
  • InstallPromptService initialization < 50ms

NFR-05 Compliance (Offline Behavior):

  • Install prompt works offline after first load
  • Manifest is cached by service worker
  • Icons are cached by service worker

Security & Privacy Requirements

Manifest Security:

  • start_url should use HTTPS
  • No sensitive data in manifest
  • scope should limit app's reach

Install Prompt Safety:

  • beforeinstallprompt event is browser-controlled (secure)
  • User must explicitly accept install (no forced installs)
  • Install happens on user's device (local-only)

Project Structure Notes

Following Feature-First Lite Pattern:

src/
  components/
    features/
      pwa/                 # NEW: PWA-specific components
        InstallPromptButton.tsx
        index.ts
  lib/
    store/
      install-prompt-store.ts    # NEW: Install prompt state
      engagement-store.ts        # NEW: Session tracking (or reuse existing)
  services/
    install-prompt-service.ts    # NEW: Install prompt logic
    engagement-tracker.ts        # NEW: Engagement detection
  app/
    manifest.ts                  # NEW: PWA manifest (Next.js 16+)
    layout.tsx                   # MODIFY: Initialize service

Public Assets:

public/
  icons/
    icon-192x192.png    # NEW: 192x192 app icon
    icon-512x512.png    # NEW: 512x512 app icon
    maskable-icon.png   # OPTIONAL: Maskable icon for Android

Files to Create:

  • src/app/manifest.ts - PWA manifest configuration
  • src/lib/store/install-prompt-store.ts - Install prompt state management
  • src/lib/store/install-prompt-store.test.ts - Store tests
  • src/services/install-prompt-service.ts - Install prompt service
  • src/services/install-prompt-service.test.ts - Service tests
  • src/components/features/pwa/InstallPromptButton.tsx - Install button component
  • src/components/features/pwa/InstallPromptButton.test.tsx - Button tests
  • src/components/features/pwa/index.ts - Feature exports
  • src/services/engagement-tracker.ts - Engagement detection service
  • src/services/engagement-tracker.test.ts - Engagement tests

Files to Modify:

  • src/app/layout.tsx - Initialize InstallPromptService, add manifest metadata
  • next.config.ts - Ensure PWA support is enabled

Icon Assets to Create:

  • public/icons/icon-192x192.png - 192x192 app icon
  • public/icons/icon-512x512.png - 512x512 app icon

Browser Compatibility Notes

beforeinstallprompt Support:

  • Chrome/Edge: Supported (Desktop & Android)
  • Firefox: Supported (Desktop)
  • Safari: NOT supported (iOS or Desktop)

iOS Safari Fallback: iOS doesn't support beforeinstallprompt. Users must:

  1. Tap Share button
  2. Scroll down and tap "Add to Home Screen"
  3. Tap "Add"

For iOS, show a subtle help icon or tooltip with instructions.

Standalone Detection:

// Chrome/Edge/Firefox
window.matchMedia('(display-mode: standalone)').matches

// iOS Safari
window.navigator.standalone === true

Latest Technical Information (2026)

Next.js 16 PWA Support (Jan 2026):

  • Use src/app/manifest.ts for manifest generation
  • Next.js automatically generates manifest.json at build time
  • No need for manual manifest.json file in public
  • Metadata API handles manifest linking

beforeinstallprompt Event (2026):

  • Still the standard for custom install prompts
  • Event fires only when PWA criteria are met
  • Must prevent default to show custom UI later
  • Event is valid until user dismisses or installs

PWA Installability Criteria (2026):

  1. Valid manifest.json with name, short_name, icons
  2. Service Worker registered (from story 3.3)
  3. HTTPS served (or localhost for development)
  4. At least one visit to site (no first-install prompts)

Recent Changes:

  • Chrome 130+: Improved install heuristics (fewer automatic prompts)
  • Safari 18+: Still no beforeinstallprompt, but improved "Add to Home Screen" UX
  • Edge: Same as Chrome (Chromium-based)

References

Epic Reference:

Architecture Documents:

Previous Stories:

External References:

Dev Agent Record

Agent Model Used

Claude Opus 4.5 (model ID: 'claude-opus-4-5-20251101')

Debug Log References

Session file: /tmp/claude/-home-maximilienmao-Projects-Test01/edb6d0a1-65e2-4871-b93f-126aaba44907/scratchpad

Completion Notes List

Story Analysis Completed:

  • Extracted story requirements from Epic 3, Story 3.4
  • Analyzed Stories 3.3, 3.2, 3.1 for established patterns
  • Reviewed architecture for Service Layer and State Management compliance
  • Researched latest Next.js 16 PWA patterns and beforeinstallprompt best practices
  • Identified all files to create and modify

Implementation Context Summary:

Story Purpose: This story implements PWA installability with a custom install prompt. It enables users to install Test01 to their home screen for a native-app experience. The custom prompt appears only after user engagement (1+ completed sessions) for better UX than the browser's default prompt.

Key Technical Decisions:

  1. Manifest Generation: Use Next.js 16's src/app/manifest.ts (built-in support)
  2. InstallPromptService: Service layer for beforeinstallprompt event handling
  3. InstallPromptStore: Zustand store for install state management
  4. Engagement Detection: Use completed drafts count as engagement metric
  5. Non-Intrusive UI: Fixed bottom-right button, not modal/overlay
  6. iOS Fallback: Show "Add to Home Screen" instructions (iOS doesn't support beforeinstallprompt)

Dependencies:

  • No new external dependencies required
  • Uses existing Zustand for state management
  • Uses existing Dexie for engagement tracking (drafts count)
  • Browser's beforeinstallprompt API

Integration Points:

  • InstallPromptService initialized in app layout
  • Manifest.ts generates manifest.json automatically
  • InstallPromptButton in layout (conditionally rendered)
  • Engagement tracker uses existing drafts table

Files to Create:

  • src/app/manifest.ts - PWA manifest configuration
  • src/lib/store/install-prompt-store.ts - Install state management
  • src/services/install-prompt-service.ts - Install prompt service
  • src/components/features/pwa/InstallPromptButton.tsx - Install button
  • src/services/engagement-tracker.ts - Engagement detection
  • Test files for all above

Files to Modify:

  • src/app/layout.tsx - Initialize service, add metadata

Icon Assets:

  • public/icons/icon-192x192.png
  • public/icons/icon-512x512.png

PWA Install Data Flow:

App loads → InstallPromptService.initialize()
    ↓
Check standalone mode (isInstalled)
    ↓
Add beforeinstallprompt listener
    ↓
Browser fires event (when PWA criteria met)
    ↓
Save event to InstallPromptStore, set isInstallable=true
    ↓
EngagementTracker checks session count
    ↓
InstallPromptButton appears (isInstallable && !isInstalled && sessions>0)
    ↓
User clicks button → promptInstall() calls event.prompt()
    ↓
User accepts → isInstalled=true, prompt hidden

Browser Support Matrix:

  • Chrome/Edge (Desktop/Android): Full support (beforeinstallprompt + custom UI)
  • Firefox (Desktop): Full support
  • Safari (Desktop/iOS): No beforeinstallprompt → Show manual instructions

MVP Scope:

  • Basic install prompt with engagement detection
  • Icon assets (192x192, 512x512)
  • Standalone mode detection
  • iOS fallback instructions

Post-MVP Enhancements:

  • Maskable icons for Android adaptive shape
  • Custom install splash screen
  • Install prompt A/B testing (timing, messaging)
  • In-app ratings prompt after install
  • Deferred install timing (after N sessions)

Implementation Summary: All automated tests pass (77 tests):

  • src/app/manifest.test.ts: 11 tests passed
  • src/lib/store/install-prompt-store.test.ts: 21 tests passed
  • src/services/install-prompt-service.test.ts: 19 tests passed
  • src/services/engagement-tracker.test.ts: 17 tests passed
  • src/components/features/pwa/InstallPromptButton.test.tsx: 9 tests passed

Manual browser tests remain to be done during QA phase.


File List

New Files Created:

  • src/app/manifest.ts - PWA manifest configuration
  • src/app/manifest.test.ts - Manifest tests
  • src/lib/store/install-prompt-store.ts - Install prompt state management (Zustand)
  • src/lib/store/install-prompt-store.test.ts - Store tests
  • src/services/install-prompt-service.ts - Install prompt service layer
  • src/services/install-prompt-service.test.ts - Service tests
  • src/services/engagement-tracker.ts - Engagement detection service
  • src/services/engagement-tracker.test.ts - Engagement tests
  • src/components/features/pwa/InstallPromptButton.tsx - Install button component
  • src/components/features/pwa/InstallPromptButton.test.tsx - Button tests
  • src/components/features/pwa/index.ts - Feature exports
  • public/icons/icon-192x192.png - 192x192 PWA icon
  • public/icons/icon-512x512.png - 512x512 PWA icon

Modified Files:

  • src/app/layout.tsx - Added PWA metadata, InstallPromptService initialization, InstallPromptButton rendering

Change Log

Date: 2026-01-23

Implemented PWA Install Prompt & Manifest (Story 3.4)

  • Created Next.js 16 compatible manifest.ts with proper PWA configuration
  • Created InstallPromptStore for state management using Zustand with atomic selectors
  • Created InstallPromptService following Logic Sandwich pattern for event handling
  • Created EngagementTracker using completed drafts as engagement metric
  • Created InstallPromptButton component with non-intrusive fixed bottom-right positioning
  • Updated layout.tsx with PWA metadata and service initialization
  • Added placeholder icon assets (192x192, 512x512)
  • All 77 automated tests passing

Code Review Update (Senior Dev AI) - 2026-01-23

  • Fixed: Broke Server-Side Rendering in layout.tsx by moving service initialization to PWAInitializer.tsx client component.
  • Fixed: Added missing experimental optimizePackageImports to next.config.ts.
  • Refactored: Updated InstallPromptButton.tsx to use InstallPromptService.promptInstall() directly, adhering strictly to the Logic Sandwich pattern.
  • Verified: All architectural patterns now fully compliant.

Code Review Update (Senior Dev AI) - 2026-01-24

  • Fixed: Test mock in InstallPromptButton.test.tsx was missing promptInstall method, causing test failure.
  • Fixed: Removed duplicate "Atomic selectors for performance" comment in InstallPromptButton.tsx.
  • Fixed: Replaced placeholder 1-bit colormap icons with real Morning Mist themed PWA icons (192x192: ~37KB, 512x512: ~337KB).
  • Fixed: PWAInitializer.test.tsx was using Jest syntax (jest.mock) instead of Vitest syntax (vi.mock), causing test suite to fail.
  • Synced: Updated sprint-status.yaml to match story status (done).
  • Verified: All 78 PWA-related tests now passing (10 in pwa/, 22 in store, 19 in service, 17 in engagement, 11 in manifest).

Architecture Compliance:

  • Logic Sandwich Pattern: UI -> Store -> Service (no direct event handling in components)
  • Atomic Selectors: All Zustand stores use individual property selectors
  • Local-First: Engagement data stored in IndexedDB (drafts table)

Implementation Plan

Phase 1: Foundation (Completed)

  • Created PWA manifest configuration (src/app/manifest.ts)
  • Created placeholder icon assets
  • Updated layout metadata for PWA support

Phase 2: State & Service Layer (Completed)

  • Created InstallPromptStore with atomic selectors
  • Created InstallPromptService for beforeinstallprompt event handling
  • Created EngagementTracker for engagement detection

Phase 3: UI Components (Completed)

  • Created InstallPromptButton with conditional rendering
  • Integrated button into app layout

Phase 4: Testing (Completed - Automated)

  • Unit tests for store, service, engagement tracker
  • Component tests for InstallPromptButton
  • All 77 tests passing

Phase 5: Manual Testing (Pending)

  • Chrome Desktop install flow
  • Chrome Android install flow
  • Standalone mode verification
  • iOS fallback behavior
  • Lighthouse PWA audit