Skip to content

feat(web): make the web app an installable PWA - #193

Merged
enaboapps merged 1 commit into
mainfrom
codex/pwa-192
Oct 9, 2026
Merged

enaboapps merged 1 commit into
mainfrom
codex/pwa-192

Conversation

@enaboapps

Copy link
Copy Markdown
Contributor

Closes #192

Summary

  • Manifest (public/manifest.json): name, start URL and scope /, standalone display, #050505 theme and background, and 192, 512 and maskable icons in public/icons/. The icons come from the existing app icons; the maskable one uses the Android adaptive foreground, so it stays inside the safe zone.
  • Root HTML (src/app/+html.tsx): Expo's default document plus the manifest link, theme colour, description and Apple touch icon.
  • Service worker (public/sw.js, no new dependency): pages are fetched network-first with a cached fallback, so deployments are never stuck behind the cache. Content-hashed /_expo/static/ bundles and /assets/ are cached once. It is registered only in production exports, so npm run web never caches Metro bundles.
  • Vercel (vercel.json): no-cache for sw.js and the manifest (served as application/manifest+json), and immutable caching for hashed bundles.
  • Docs: an "Installing as an app" section in docs/web.md.

Native builds are unchanged.

Validation

  • npm run validate: lint, typecheck, 673 Jest tests and 21/21 Expo Doctor checks pass.
  • On a production npx expo export -p web served locally, in headless Chrome:
    • Page.getAppManifest reports no manifest errors.
    • With a normal profile, Page.getInstallabilityErrors reports none.
    • The service worker activates and controls the page after a reload.
    • With the network offline, reloading / and opening /settings still render the app.
  • A fresh expo start --web page links the manifest but does not register the service worker.

🤖 Generated with Claude Code

- Add a web app manifest with standalone display, theme colours and 192, 512 and
  maskable icons generated from the existing app icons, linked from a root +html.tsx
- Add a dependency-free service worker: pages load network-first with an offline
  fallback, and content-hashed Expo bundles and assets are cached once
- Register it only in production exports so development never caches Metro bundles
- Serve sw.js and the manifest with no-cache and hashed bundles as immutable on Vercel
- Document installation and caching in docs/web.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
switchify-remote Ready Ready Preview Oct 9, 2026 10:01am UTC

Request Review

@enaboapps
enaboapps marked this pull request as ready for review October 9, 2026 10:15
@enaboapps
enaboapps merged commit 808c51d into main Oct 9, 2026
5 checks passed
@greptile-apps

greptile-apps Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Medium impact] Adds service worker and PWA installation support to the web app.

Do not merge until cache-write failures stop discarding successful network responses; the first-installation offline gap is a separate non-blocking concern.

Findings

  1. P1 Full storage breaks online loading ▶
  2. P2 Installation misses required app code ▶
Fix with agent prompt
### Issue 1
public/sw.js:23-25
If browser storage is full, `cache.put` can fail after a successful download. `networkFirst` then returns an old cached page, while `cacheFirst` rejects the bundle request instead of returning the downloaded code. Users can get an old build or an app that fails to load even while online. Catch cache-write failures separately in both functions and still return the downloaded response. This loading failure must be fixed before merging.

### Issue 2
public/sw.js:5-9
Installation saves the root HTML but none of its required JavaScript bundles. `registerServiceWorker` runs after `load`, when the first visit's bundles have already loaded without the worker. Until another controlled visit, offline startup depends on the browser's separate HTTP cache. If that cache is gone, the installed app cannot open offline. This is a non-blocking offline reliability gap: save the required bundles during installation as well.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

T-Rex evidence

Evidence from the check

  • This executed harness serves the unchanged worker and runs isolated baseline, healthy-cache, and injected-failure cases, making the reproduction inspectable.

Evidence from the check

  • This executed runner records each command, working directory, exit code, and observed output, making the verification reproducible.

Command output from the check

  • The command captured the commit, repository status, worker hash, full worker source, and its absence at base, identifying exactly what was tested.

Command output from the check

  • Chromium loaded the same HTTP fixtures without service-worker registration and received fresh HTML plus an executable bundle, establishing the no-worker baseline.

Command output from the check

  • Chromium executed the unchanged worker with normal Cache.put behavior and received fresh HTML plus an executable bundle, confirming the healthy control.

Command output from the check

  • Chromium executed the unchanged worker with injected Cache.put rejections and observed stale HTML plus failed bundle loading despite HTTP 200 fetches, supporting candidate 0.

Evidence from the check

  • This executed script serves the prepared build, records both launches and asserts bundle failures and preserved CacheStorage, reproducing the missing-code installation defect.

Evidence from the check

  • This executed runner captures the reproduction command, working directory, exit code and actual output into the execution log, making the run reproducible.

Command output from the check

  • The captured successful command output includes browser network events, DOM state and CacheStorage entries for both launches, supporting the reported defect.

▶ Recording of the check

  • Chromium records the fresh production visit and holds on the rendered onboarding UI for eight seconds, establishing the working baseline.

Screenshot of the first online visit

  • The screenshot captures the first-visit page after live DOM assertions detected onboarding text and two buttons, documenting the working baseline.

▶ Recording of the check

  • Chromium records the offline launch and holds its nonfunctional result for ten seconds after the required bundle fails, demonstrating the installation gap.

Screenshot of the nonfunctional offline launch

  • The screenshot captures the offline page whose live DOM contains only “Switchify Remote” and no buttons, documenting the failed app render.

Evidence from the check

  • This executed harness serves the unchanged worker and runs isolated baseline, healthy-cache, and injected-failure cases, making the reproduction inspectable.

Evidence from the check

  • This executed runner records each command, working directory, exit code, and observed output, making the verification reproducible.

Command output from the check

  • The command captured the commit, repository status, worker hash, full worker source, and its absence at base, identifying exactly what was tested.

Command output from the check

  • Chromium loaded the same HTTP fixtures without service-worker registration and received fresh HTML plus an executable bundle, establishing the no-worker baseline.

Command output from the check

  • Chromium executed the unchanged worker with normal Cache.put behavior and received fresh HTML plus an executable bundle, confirming the healthy control.

Command output from the check

  • Chromium executed the unchanged worker with injected Cache.put rejections and observed stale HTML plus failed bundle loading despite HTTP 200 fetches, supporting candidate 0.

Before: Pr193 Install Offline 01

  • What the screen looked like at this point in the check.

▶ pr193-install-offline-01-before.webm

  • The flow T-Rex ran, recorded end to end.

After: Pr193 Install Offline 02

  • What the screen looked like at this point in the check.

▶ pr193-install-offline-02-after.webm

  • The flow T-Rex ran, recorded end to end.

Evidence from the check

  • This executed script serves the prepared build, records both launches and asserts bundle failures and preserved CacheStorage, reproducing the missing-code installation defect.

Evidence from the check

  • This executed runner captures the reproduction command, working directory, exit code and actual output into the execution log, making the run reproducible.

Command output from the check

  • The captured successful command output includes browser network events, DOM state and CacheStorage entries for both launches, supporting the reported defect.

▶ Recording of the check

  • Chromium records the fresh production visit and holds on the rendered onboarding UI for eight seconds, establishing the working baseline.

Screenshot of the first online visit

  • The screenshot captures the first-visit page after live DOM assertions detected onboarding text and two buttons, documenting the working baseline.

▶ Recording of the check

  • Chromium records the offline launch and holds its nonfunctional result for ten seconds after the required bundle fails, demonstrating the installation gap.

Screenshot of the nonfunctional offline launch

  • The screenshot captures the offline page whose live DOM contains only “Switchify Remote” and no buttons, documenting the failed app render.

View artifacts

Summary

Adds an installable web app with a manifest, icons, a production-only service worker, Vercel cache headers and installation docs.

  • Chrome and Edge can install Switchify Remote as a standalone app.
  • The web app can reopen its cached pages when offline.

Changes are required in public/sw.js before merging to ensure cache-write failures cannot break otherwise successful online loading.

Reviews (1) · Last reviewed commit: "feat(web): make the web app an installab..." · Reviewed by Greptile

Comment thread public/sw.js
Comment on lines +23 to +25
const response = await fetch(request);
if (response.ok) await cache.put(request, response.clone());
return response;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Full storage breaks online loading

If browser storage is full, cache.put can fail after a successful download. networkFirst then returns an old cached page, while cacheFirst rejects the bundle request instead of returning the downloaded code. Users can get an old build or an app that fails to load even while online. Catch cache-write failures separately in both functions and still return the downloaded response. This loading failure must be fixed before merging.

Artifacts

Evidence from the check

  • This executed harness serves the unchanged worker and runs isolated baseline, healthy-cache, and injected-failure cases, making the reproduction inspectable.

Evidence from the check

  • This executed runner records each command, working directory, exit code, and observed output, making the verification reproducible.

Command output from the check

  • The command captured the commit, repository status, worker hash, full worker source, and its absence at base, identifying exactly what was tested.

Command output from the check

  • Chromium loaded the same HTTP fixtures without service-worker registration and received fresh HTML plus an executable bundle, establishing the no-worker baseline.

Command output from the check

  • Chromium executed the unchanged worker with normal Cache.put behavior and received fresh HTML plus an executable bundle, confirming the healthy control.

Command output from the check

  • Chromium executed the unchanged worker with injected Cache.put rejections and observed stale HTML plus failed bundle loading despite HTTP 200 fetches, supporting candidate 0.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: public/sw.js
Line: 23-25

Comment:
**Full storage breaks online loading**

If browser storage is full, `cache.put` can fail after a successful download. `networkFirst` then returns an old cached page, while `cacheFirst` rejects the bundle request instead of returning the downloaded code. Users can get an old build or an app that fails to load even while online. Catch cache-write failures separately in both functions and still return the downloaded response. This loading failure must be fixed before merging.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread public/sw.js
Comment on lines +5 to +9
const SHELL = ['/', '/manifest.json', '/icons/icon-192.png', '/icons/icon-512.png'];
const IMMUTABLE = ['/_expo/static/', '/assets/'];

self.addEventListener('install', (event) => {
event.waitUntil(caches.open(CACHE).then((cache) => cache.addAll(SHELL)).then(() => self.skipWaiting()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Installation misses required app code

Installation saves the root HTML but none of its required JavaScript bundles. registerServiceWorker runs after load, when the first visit's bundles have already loaded without the worker. Until another controlled visit, offline startup depends on the browser's separate HTTP cache. If that cache is gone, the installed app cannot open offline. This is a non-blocking offline reliability gap: save the required bundles during installation as well.

Artifacts

Evidence from the check

  • This executed script serves the prepared build, records both launches and asserts bundle failures and preserved CacheStorage, reproducing the missing-code installation defect.

Evidence from the check

  • This executed runner captures the reproduction command, working directory, exit code and actual output into the execution log, making the run reproducible.

Command output from the check

  • The captured successful command output includes browser network events, DOM state and CacheStorage entries for both launches, supporting the reported defect.

▶ Recording of the check

  • Chromium records the fresh production visit and holds on the rendered onboarding UI for eight seconds, establishing the working baseline.

Screenshot of the first online visit

  • The screenshot captures the first-visit page after live DOM assertions detected onboarding text and two buttons, documenting the working baseline.

▶ Recording of the check

  • Chromium records the offline launch and holds its nonfunctional result for ten seconds after the required bundle fails, demonstrating the installation gap.

Screenshot of the nonfunctional offline launch

  • The screenshot captures the offline page whose live DOM contains only “Switchify Remote” and no buttons, documenting the failed app render.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: public/sw.js
Line: 5-9

Comment:
**Installation misses required app code**

Installation saves the root HTML but none of its required JavaScript bundles. `registerServiceWorker` runs after `load`, when the first visit's bundles have already loaded without the worker. Until another controlled visit, offline startup depends on the browser's separate HTTP cache. If that cache is gone, the installed app cannot open offline. This is a non-blocking offline reliability gap: save the required bundles during installation as well.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

This branch was successfully deployed

1 active deployment
Preview — 1735a318 Deployed Oct 9, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make the web app an installable PWA

2 participants