PWA install & splash¶
The app is installable as a standalone mobile app on iOS (16.4+), Android, and desktop Chromium. Once installed, launches play a branded animated splash.
Install banner¶
Component: apps/web/components/pwa/InstallPrompt.tsx
Device-aware UX:
- Android / desktop Chromium — captures
beforeinstallprompt, shows a branded card with a single "Install app" button that fires the native prompt. - iOS Safari — can't programmatically install. Shows instructions: tap Share → Add to Home Screen, with matching Lucide icons.
- Already installed (
display-mode: standalone) — renders nothing.
Non-annoying heuristics:
- Doesn't appear until 8 s of engagement (first visit) or 600 ms (returning visit, ≥ 2 views stored in
localStorage). - Dismissing sets a 14-day silence in
localStorage(fp_install_dismissed_until). - Slides up from the bottom with a subtle 300 ms fade+rise. No modal overlay.
- One banner per session.
Service worker¶
apps/web/public/sw.js — minimal network-first SW that satisfies the Chrome installable criteria:
- Installs and caches a tiny "shell" (home, manifest, logo, icons).
- On fetch: tries the network first, falls back to cached shell on failure.
- Ignores cross-origin requests (Woo Store API et al.).
Registration is silent on idle via components/pwa/ServiceWorkerRegister.tsx.
Phase 2 upgrade: swap to Workbox (@ducanh2912/next-pwa) for proper offline product browsing + background sync.
Animated splash¶
Component: apps/web/components/pwa/AppSplash.tsx
Trigger conditions:
display-mode: standalone(installed PWA launch) OR?splash=1query-string override for preview in regular browsers- Only once per session (
sessionStorage.fp_splash_seen)
Sequence (~1.4 s):
| Time | What |
|---|---|
| 0 ms | Dragon-fruit mascot scales in with a bounce (0.6 → 1.12 → 1.0) |
| 250 ms | Wordmark fades in below, with 8 px lift |
| 500 ms | "Welcome to the Tropics" tagline fades in |
| 900 ms | Entire splash begins 500 ms fade out |
| 1400 ms | Splash removed from tree; content visible |
A subtle magenta radial glow pulses behind the mascot during intro, then a continuous 2.6 s breathe cycle runs while visible.
prefers-reduced-motion: reduce disables all animations.
Icons¶
All icons are generated from a single source via apps/web/scripts/generate-icons.mjs:
| File | Size | Purpose |
|---|---|---|
favicon-32.png |
32×32 | Browser tab (glyph-only at that size) |
favicon-192.png |
192×192 | Legacy browser shortcut |
icon-192.png / icon-512.png |
192 / 512 | PWA launcher (purpose: any) |
icon-maskable-192.png / -512.png |
192 / 512 | Android adaptive (safe zone: 80%) |
apple-touch-icon.png |
180×180 | iOS home screen |
All non-favicon icons render the full Fruit Plug logo on brand magenta (#be006a). The favicon falls back to the glyph-only because the wordmark becomes illegible at 32 px.
Mascot pipeline¶
The source glyph (public/brand/favicon-192.png) ships with a flattened white background — no alpha channel. To get a clean transparent mascot for the splash, apps/web/scripts/generate-mascot.mjs:
- Loads the 512×512 source
- Runs a flood-fill chroma-key from every border pixel, converting reachable white pixels to transparent (≈63% of the canvas)
- The enclosed white (fruit flesh) stays opaque because it's bounded by the black contour
- Tight-trims and upscales to 1024×1024 for retina use
Output: public/brand/mascot.png — used by the splash component.
Manifest¶
apps/web/public/manifest.webmanifest:
{
"name": "Fruit Plug",
"short_name": "Fruit Plug",
"start_url": "/",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#000000",
"orientation": "portrait",
"icons": [...]
}
background_color matches the app shell so the native OS splash (rendered before JS boots) continues seamlessly into our animated splash.