tia/src/components/InstallPrompt.tsx
Mannu b6814579c6 feat(pwa): add Serwist service worker, manifest, icons, install prompt
- Wrap next.config.ts with @serwist/next (webpack mode, disabled in dev)
- Service worker: NetworkOnly for /api/*, offline fallback → /~offline
- Web app manifest via Next.js metadata API (app/manifest.ts)
- PNG icon set generated with sharp (192, 512, maskable-512, apple-180)
- iOS meta tags: appleWebApp, themeColor viewport export
- Middleware: pwaAssets early-return so /sw.js never gets a 302→login
- Offline fallback page at /~offline (static, no auth dependency)
- InstallPrompt component: beforeinstallprompt (Android) + iOS Share sheet instructions
- Logout (menu/page.tsx): purge all SW caches on signout (shared-device safety)
- Fix invite/[token]/page.tsx params type for Next.js 16 (use(params))
- Build script: next build --webpack (Serwist requires webpack, not Turbopack)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 23:20:48 +05:30

115 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect, useState } from "react";
// Extend the BeforeInstallPromptEvent type (not in standard lib)
interface BeforeInstallPromptEvent extends Event {
prompt(): Promise<void>;
readonly userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
}
function IOSInstallInstructions({ onDismiss }: { onDismiss: () => void }) {
return (
<div className="fixed bottom-4 left-4 right-4 z-50 bg-white border border-rose-100 rounded-2xl shadow-lg p-4">
<div className="flex justify-between items-start mb-2">
<div className="flex items-center gap-2">
<span className="text-2xl">🌸</span>
<span className="font-semibold text-gray-900">Install Tia</span>
</div>
<button
onClick={onDismiss}
className="text-gray-400 text-xl leading-none p-1"
aria-label="Dismiss"
>
</button>
</div>
<p className="text-sm text-gray-600 mb-2">
Add Tia to your home screen for the best experience.
</p>
<div className="flex items-center gap-2 text-sm text-gray-700">
<span>Tap</span>
<span className="inline-flex items-center justify-center w-7 h-7 bg-gray-100 rounded-md text-base"></span>
<span>then</span>
<span className="font-medium">"Add to Home Screen"</span>
</div>
</div>
);
}
const DISMISSED_KEY = "tia_install_prompt_dismissed";
export function InstallPrompt() {
const [deferred, setDeferred] = useState<BeforeInstallPromptEvent | null>(null);
const [isIOS, setIsIOS] = useState(false);
const [dismissed, setDismissed] = useState(true); // start hidden to avoid flash
useEffect(() => {
if (typeof window === "undefined") return;
const alreadyDismissed = localStorage.getItem(DISMISSED_KEY) === "1";
if (alreadyDismissed) return;
const standalone = window.matchMedia("(display-mode: standalone)").matches;
if (standalone) return; // already installed
// iOS Safari: no beforeinstallprompt, needs manual instructions
const ios = /iphone|ipad|ipod/i.test(navigator.userAgent);
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
if (ios && isSafari) {
setIsIOS(true);
setDismissed(false);
}
// Android / Chrome: capture the deferred install event
const handler = (e: Event) => {
e.preventDefault();
setDeferred(e as BeforeInstallPromptEvent);
setDismissed(false);
};
window.addEventListener("beforeinstallprompt", handler);
return () => window.removeEventListener("beforeinstallprompt", handler);
}, []);
const handleDismiss = () => {
setDismissed(true);
localStorage.setItem(DISMISSED_KEY, "1");
};
const handleInstall = async () => {
if (!deferred) return;
await deferred.prompt();
const { outcome } = await deferred.userChoice;
if (outcome === "accepted") {
setDeferred(null);
setDismissed(true);
}
};
if (dismissed) return null;
if (deferred) {
return (
<div className="fixed bottom-4 left-4 right-4 z-50 bg-white border border-rose-100 rounded-2xl shadow-lg p-4 flex items-center gap-3">
<span className="text-2xl">🌸</span>
<div className="flex-1">
<p className="font-semibold text-gray-900 text-sm">Install Tia</p>
<p className="text-xs text-gray-500">Add to home screen for quick access</p>
</div>
<button onClick={handleDismiss} className="text-gray-400 p-1 text-lg" aria-label="Dismiss"></button>
<button
onClick={handleInstall}
className="px-4 py-2 bg-rose-400 text-white rounded-xl text-sm font-semibold"
>
Install
</button>
</div>
);
}
if (isIOS) {
return <IOSInstallInstructions onDismiss={handleDismiss} />;
}
return null;
}