Skip to content

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 ShareAdd 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:

  1. Installs and caches a tiny "shell" (home, manifest, logo, icons).
  2. On fetch: tries the network first, falls back to cached shell on failure.
  3. 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=1 query-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:

  1. Loads the 512×512 source
  2. Runs a flood-fill chroma-key from every border pixel, converting reachable white pixels to transparent (≈63% of the canvas)
  3. The enclosed white (fruit flesh) stays opaque because it's bounded by the black contour
  4. 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.