diff --git a/CHANGELOG.md b/CHANGELOG.md index 7000b7c..9be3153 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together. +## 0.64.0 — 2026-07-21 00:30 + +### Fixed +- The web app manifest was never actually linked into the page (pointed at `/manifest.json`, but the real route is `/manifest.webmanifest`) and its icons referenced PNGs that don't exist in the repo — both silently broke install eligibility on Chrome/Android. Manifest now links correctly and icons point at the existing SVGs. + +### Added +- Added apple-mobile-web-app meta tags and a `viewport-fit=cover` safe-area setting for iOS home-screen installs. +- Added an install-app banner (Chrome/Edge/Android only, via the standard `beforeinstallprompt` event) so the PWA install prompt is a first-class, dismissible UI element instead of relying on the browser's own icon. + ## 0.63.0 — 2026-07-21 00:15 ### Added diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index f8147ab..18a6eac 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -5,6 +5,7 @@ import { Providers } from "@/components/providers"; import { auth } from "@/lib/auth/server"; import type { Locale } from "@/lib/i18n/provider"; import { SwRegister } from "@/components/pwa/sw-register"; +import { InstallPrompt } from "@/components/pwa/install-prompt"; import { getMarketingLocale } from "@/lib/marketing-locale"; import "./globals.css"; @@ -22,11 +23,21 @@ const geistMono = Geist_Mono({ export const metadata: Metadata = { title: "Epicure", description: "Your personal AI-powered recipe book.", - manifest: "/manifest.json", + manifest: "/manifest.webmanifest", + icons: { + icon: "/icon.svg", + apple: "/icon.svg", + }, + appleWebApp: { + capable: true, + statusBarStyle: "default", + title: "Epicure", + }, }; export const viewport: Viewport = { themeColor: "#18181b", + viewportFit: "cover", }; export default async function RootLayout({ @@ -47,6 +58,7 @@ export default async function RootLayout({ {children} + ); diff --git a/apps/web/app/manifest.ts b/apps/web/app/manifest.ts index eba0664..40919c0 100644 --- a/apps/web/app/manifest.ts +++ b/apps/web/app/manifest.ts @@ -10,8 +10,8 @@ export default function manifest(): MetadataRoute.Manifest { background_color: "#ffffff", theme_color: "#18181b", icons: [ - { src: "/icon-192.png", sizes: "192x192", type: "image/png" }, - { src: "/icon-512.png", sizes: "512x512", type: "image/png" }, + { src: "/icon-192.svg", sizes: "192x192", type: "image/svg+xml", purpose: "any" }, + { src: "/icon-512.svg", sizes: "512x512", type: "image/svg+xml", purpose: "any" }, ], }; } diff --git a/apps/web/components/pwa/install-prompt.tsx b/apps/web/components/pwa/install-prompt.tsx new file mode 100644 index 0000000..f80c8f2 --- /dev/null +++ b/apps/web/components/pwa/install-prompt.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { X, Download } from "lucide-react"; +import { Button } from "@/components/ui/button"; + +const DISMISSED_KEY = "epicure:install-prompt-dismissed"; + +type BeforeInstallPromptEvent = Event & { + prompt: () => Promise; + userChoice: Promise<{ outcome: "accepted" | "dismissed" }>; +}; + +/** Only fires on Chromium-based browsers (Chrome/Edge/Android) — Safari and + * Firefox never dispatch beforeinstallprompt, so this banner simply never + * appears there, which is the correct behavior (no fallback UI to build). */ +export function InstallPrompt() { + const [deferredEvent, setDeferredEvent] = useState(null); + const [dismissed, setDismissed] = useState(() => typeof localStorage !== "undefined" && localStorage.getItem(DISMISSED_KEY) === "true"); + + useEffect(() => { + function handler(e: Event) { + e.preventDefault(); + setDeferredEvent(e as BeforeInstallPromptEvent); + } + window.addEventListener("beforeinstallprompt", handler); + return () => window.removeEventListener("beforeinstallprompt", handler); + }, []); + + function dismiss() { + localStorage.setItem(DISMISSED_KEY, "true"); + setDismissed(true); + } + + async function install() { + if (!deferredEvent) return; + await deferredEvent.prompt(); + await deferredEvent.userChoice; + setDeferredEvent(null); + } + + if (!deferredEvent || dismissed) return null; + + return ( +
+ +
+

Install Epicure

+

Add to your home screen for quick access.

+
+ + +
+ ); +} diff --git a/apps/web/lib/changelog.ts b/apps/web/lib/changelog.ts index 61cf1ec..c126c03 100644 --- a/apps/web/lib/changelog.ts +++ b/apps/web/lib/changelog.ts @@ -1,5 +1,5 @@ // Mirrors CHANGELOG.md at the repo root — update both together. -export const APP_VERSION = "0.63.0"; +export const APP_VERSION = "0.64.0"; export type ChangelogEntry = { version: string; @@ -11,6 +11,17 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + version: "0.64.0", + date: "2026-07-21 00:30", + fixed: [ + "The web app manifest was never actually linked into the page (pointed at /manifest.json, but the real route is /manifest.webmanifest) and its icons referenced PNGs that don't exist in the repo — both silently broke install eligibility on Chrome/Android. Manifest now links correctly and icons point at the existing SVGs.", + ], + added: [ + "Added apple-mobile-web-app meta tags and a viewport-fit=cover safe-area setting for iOS home-screen installs.", + "Added an install-app banner (Chrome/Edge/Android only, via the standard beforeinstallprompt event) so the PWA install prompt is a first-class, dismissible UI element instead of relying on the browser's own icon.", + ], + }, { version: "0.63.0", date: "2026-07-21 00:15", diff --git a/apps/web/package.json b/apps/web/package.json index 11c1a6d..1477ac6 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@epicure/web", - "version": "0.63.0", + "version": "0.64.0", "private": true, "scripts": { "dev": "next dev", diff --git a/package.json b/package.json index 9a819f2..6bf91e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "epicure", - "version": "0.63.0", + "version": "0.64.0", "private": true, "scripts": { "dev": "pnpm --filter web dev",