nexus/.planning/phases/26-pwa-performance/26-03-PLAN.md

12 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
26-pwa-performance 03 execute 3
26-00
26-02
ui/src/components/InstallPromptBanner.tsx
ui/src/components/OfflineBanner.tsx
ui/src/hooks/useInstallPrompt.ts
ui/src/hooks/useOfflineQueue.ts
ui/src/hooks/useOnlineStatus.ts
ui/src/components/ChatPanel.tsx
true
PWA-01
PWA-02
PWA-08
truths artifacts key_links
Install prompt banner appears after beforeinstallprompt fires and user has visited a conversation
Install prompt is dismissable and respects 7-day localStorage cooldown
On iOS (no beforeinstallprompt), banner shows Share menu instructions
Offline banner appears when navigator.onLine is false, showing queued message count
Offline banner auto-dismisses 3 seconds after reconnection when queue is empty
Unsent messages are stored in IndexedDB and flushed when online event fires
path provides min_lines
ui/src/components/InstallPromptBanner.tsx PWA install prompt UI 40
path provides min_lines
ui/src/components/OfflineBanner.tsx Offline status banner with queue count 20
path provides min_lines
ui/src/hooks/useInstallPrompt.ts Captures beforeinstallprompt event 20
path provides min_lines
ui/src/hooks/useOfflineQueue.ts IndexedDB message queue with flush on reconnect 40
path provides min_lines
ui/src/hooks/useOnlineStatus.ts navigator.onLine reactive state 10
from to via pattern
ui/src/hooks/useOfflineQueue.ts idb openDB for IndexedDB access openDB
from to via pattern
ui/src/components/InstallPromptBanner.tsx ui/src/hooks/useInstallPrompt.ts canInstall + promptInstall from hook useInstallPrompt
from to via pattern
ui/src/components/OfflineBanner.tsx ui/src/hooks/useOnlineStatus.ts isOnline state for show/hide useOnlineStatus
Create the PWA install prompt banner, offline status banner, and offline message queue. Users see an install prompt after engagement, an amber banner when offline with queued message count, and messages auto-send when back online.

Purpose: Delivers PWA-01 (offline capability with message queuing), PWA-02 (installable manifest — already done, this plan adds the install UI), and PWA-08 (Add to Home Screen prompt). Output: 2 new components, 3 new hooks, ChatPanel integration for offline queue.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/26-pwa-performance/26-RESEARCH.md @.planning/phases/26-pwa-performance/26-UI-SPEC.md @.planning/phases/26-pwa-performance/26-02-SUMMARY.md @ui/src/components/ChatPanel.tsx @ui/src/api/chat.ts From ui/src/api/chat.ts (chatApi used by offline queue): ```typescript // chatApi.postMessage or equivalent POST method for sending messages // The offline queue needs to know the POST shape to replay messages ```

From ui/src/types/pwa.d.ts (created in Plan 00):

interface BeforeInstallPromptEvent extends Event {
  prompt(): Promise<void>;
  userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
}
Task 1: Create useInstallPrompt, useOnlineStatus, useOfflineQueue hooks ui/src/hooks/useInstallPrompt.ts, ui/src/hooks/useOnlineStatus.ts, ui/src/hooks/useOfflineQueue.ts - ui/src/types/pwa.d.ts - ui/src/api/chat.ts - .planning/phases/26-pwa-performance/26-RESEARCH.md 1. Create `ui/src/hooks/useOnlineStatus.ts`: - Export `function useOnlineStatus(): boolean` - Use `useState(navigator.onLine)` for initial state - `useEffect` adding `online` and `offline` event listeners on `window` - Return `isOnline`
  1. Create ui/src/hooks/useInstallPrompt.ts:

    • Export function useInstallPrompt(): { canInstall: boolean; promptInstall: () => Promise<void>; isIOS: boolean }
    • Use useState<BeforeInstallPromptEvent | null>(null) for deferred prompt
    • useEffect listening for beforeinstallprompt on window: call e.preventDefault(), store event
    • isInstalled: check window.matchMedia("(display-mode: standalone)").matches
    • isIOS: detect via navigator.userAgent containing "iPhone" or "iPad" and not "CriOS" (Chrome on iOS)
    • promptInstall: call deferredPrompt.prompt(), await deferredPrompt.userChoice, set deferred to null
    • canInstall: !!deferredPrompt && !isInstalled
    • Return { canInstall, promptInstall, isIOS }
  2. Create ui/src/hooks/useOfflineQueue.ts:

    • Import { openDB } from "idb"
    • Constants: DB_NAME = "nexus-offline", STORE = "message_queue"
    • Export function useOfflineQueue(): { enqueue: (conversationId: string, content: string) => Promise<void>; flush: () => Promise<void>; queuedCount: number }
    • Use useState(0) for queuedCount
    • getDb helper: openDB(DB_NAME, 1, { upgrade(db) { db.createObjectStore(STORE, { autoIncrement: true }); } })
    • enqueue callback: opens db, adds { conversationId, content, queuedAt: Date.now() } to store, increments queuedCount
    • flush callback: opens db, gets all entries and keys, iterates sequentially: a. For each entry, call chatApi.sendMessage(entry.conversationId, { content: entry.content }) (read chatApi to find the correct method name) b. On success, delete the key from the store, decrement queuedCount c. On failure, break — stop flushing, retry next time
    • useEffect listening for online event on window — calls flush()
    • useEffect on mount — reads current queue count from IndexedDB and sets queuedCount
    • Return { enqueue, flush, queuedCount } grep -q "openDB" ui/src/hooks/useOfflineQueue.ts && grep -q "beforeinstallprompt" ui/src/hooks/useInstallPrompt.ts && grep -q "navigator.onLine" ui/src/hooks/useOnlineStatus.ts && echo "PASS" <acceptance_criteria>
    • useOnlineStatus.ts returns boolean based on navigator.onLine
    • useInstallPrompt.ts captures beforeinstallprompt event and returns canInstall, promptInstall, isIOS
    • useOfflineQueue.ts uses idb library's openDB, stores to nexus-offline DB, message_queue store
    • useOfflineQueue.ts flushes on online event, stops on first failure
    • pnpm --filter @paperclipai/ui build succeeds </acceptance_criteria> Three hooks created: useOnlineStatus (reactive online/offline state), useInstallPrompt (beforeinstallprompt capture with iOS detection), useOfflineQueue (IndexedDB queue with auto-flush).
Task 2: Create InstallPromptBanner, OfflineBanner, and wire into ChatPanel ui/src/components/InstallPromptBanner.tsx, ui/src/components/OfflineBanner.tsx, ui/src/components/ChatPanel.tsx - ui/src/hooks/useInstallPrompt.ts - ui/src/hooks/useOnlineStatus.ts - ui/src/hooks/useOfflineQueue.ts - ui/src/components/ChatPanel.tsx - .planning/phases/26-pwa-performance/26-UI-SPEC.md 1. Create `ui/src/components/InstallPromptBanner.tsx`: - Import `useInstallPrompt` hook - Props: none (self-contained) - Internal state: `dismissed` from `localStorage.getItem("nexus.installPromptDismissed")` - Show conditions (ALL must be true): a. `canInstall === true` OR `isIOS === true` (iOS gets instruction text) b. Not already installed (`display-mode: standalone` check is in the hook) c. Not dismissed within last 7 days (check `nexus.installPromptDismissed` timestamp in localStorage) d. User has visited at least one conversation (pass `hasEngaged` as prop or check localStorage) - Layout (per UI-SPEC): a. Fixed position: `fixed bottom-16 left-4 right-4 md:bottom-auto md:top-4 md:left-auto md:right-4 md:max-w-sm` (bottom on mobile above MobileBottomNav, top-right on desktop) b. Background: `bg-card border border-border rounded-lg shadow-lg p-4` c. Heading: "Add Nexus to your home screen" in `text-sm font-semibold` d. Body: "Get the full experience — launch instantly, works offline." in `text-xs text-muted-foreground` e. For iOS: body text changes to "Open the Share menu and tap 'Add to Home Screen'" f. CTA button: `` — calls `promptInstall()` (or no-op on iOS) g. Dismiss button: "Not now" as `