Skip to content

Harden PWA refresh behavior on iPhone - #383

Open
kinoppyd wants to merge 2 commits into
mainfrom
fixes/pwa
Open

Harden PWA refresh behavior on iPhone#383
kinoppyd wants to merge 2 commits into
mainfrom
fixes/pwa

Conversation

@kinoppyd

Copy link
Copy Markdown
Member

Introduce a shared PWA version helper and use versioned URLs for the manifest, service worker registration path, and apple-touch-icon links so iOS and Safari are forced to see updated metadata and icons.

Update the service worker to precache during install, delete old named caches during activate, claim clients immediately, and read cached responses only from the current cache instead of falling back to stale entries from previous cache names.

Mark manifest, service worker, and icon endpoints as non-cacheable in the service worker so browser metadata refreshes are not blocked by app-level caching.

Register the service worker with updateViaCache set to none and trigger an explicit registration update on load to reduce stale worker reuse.

Replace the empty public apple-touch-icon files with a real PNG so iOS home screen icons no longer resolve to invalid placeholder files.

Introduce a shared PWA version helper and use versioned URLs for the manifest, service worker registration path, and apple-touch-icon links so iOS and Safari are forced to see updated metadata and icons.

Update the service worker to precache during install, delete old named caches during activate, claim clients immediately, and read cached responses only from the current cache instead of falling back to stale entries from previous cache names.

Mark manifest, service worker, and icon endpoints as non-cacheable in the service worker so browser metadata refreshes are not blocked by app-level caching.

Register the service worker with updateViaCache set to none and trigger an explicit registration update on load to reduce stale worker reuse.

Replace the empty public apple-touch-icon files with a real PNG so iOS home screen icons no longer resolve to invalid placeholder files.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to reduce stale PWA metadata/assets on iOS/Safari by introducing a shared PWA version helper, versioning key PWA URLs (manifest, service worker registration, icons), and tightening service worker caching/refresh behavior.

Changes:

  • Add pwa_version/versioned_path helper and apply it to manifest, service worker registration, and apple-touch-icon links.
  • Update the service worker to precache on install, clean up caches on activate, and avoid reading from old cache names.
  • Replace placeholder apple-touch-icon files with real PNGs.

Reviewed changes

Copilot reviewed 5 out of 7 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
public/apple-touch-icon.png Adds a real apple-touch icon PNG asset.
public/apple-touch-icon-precomposed.png Adds a real precomposed apple-touch icon PNG asset.
app/views/pwa/service-worker.js.erb Updates cache versioning, install/activate lifecycle, and cache lookup behavior.
app/views/pwa/manifest.json.erb Versions manifest icon URLs via versioned_path.
app/views/layouts/application.html.erb Adds versioned manifest/SW path metadata and apple-touch-icon link tags.
app/javascript/application.js Registers SW with updateViaCache: 'none' and forces registration.update().
app/helpers/application_helper.rb Introduces PWA version constant + versioned_path helper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +20 to +22
"/icon.png",
"/apple-touch-icon",
"/favicon.ico"

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

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

/favicon.ico is marked non-cacheable, but the layout uses an event-scoped favicon path (e.g. /<event>/favicon.ico). Those requests won’t match this entry and may still be cached by the SW. Consider matching pathname.endsWith('/favicon.ico') or adding the scoped path(s) so favicon refresh isn’t blocked.

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +40
const keys = await caches.keys();
await Promise.all(keys.filter((key) => key !== cacheName).map((key) => caches.delete(key)));
await precache();

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

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

During activate, this deletes every Cache Storage entry for the origin except the current cacheName. That’s fine if this SW is the only Cache Storage user, but it will also wipe any unrelated caches (present or future). Consider scoping deletions to caches created by this SW (e.g., a name prefix) instead of deleting all non-matching keys.

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +16
<link rel="apple-touch-icon" sizes="180x180" href="<%= versioned_path('/icon.png') %>">
<link rel="apple-touch-icon-precomposed" href="<%= versioned_path('/icon.png') %>">

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

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

The layout’s apple-touch-icon / apple-touch-icon-precomposed links point to the versioned /icon.png, but this PR also adds public/apple-touch-icon.png and public/apple-touch-icon-precomposed.png. Either update these links to reference the new apple-touch-icon files (and version them) or drop the unused files to avoid confusion and ensure iOS is actually requesting the intended endpoints.

Suggested change
<link rel="apple-touch-icon" sizes="180x180" href="<%= versioned_path('/icon.png') %>">
<link rel="apple-touch-icon-precomposed" href="<%= versioned_path('/icon.png') %>">
<link rel="apple-touch-icon" sizes="180x180" href="<%= versioned_path('/apple-touch-icon.png') %>">
<link rel="apple-touch-icon-precomposed" href="<%= versioned_path('/apple-touch-icon-precomposed.png') %>">

Copilot uses AI. Check for mistakes.
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
const serviceWorkerPath = document.querySelector('meta[name="pwa-service-worker-path"]')?.content || '/service-worker.js'

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

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

Missing semicolon after the serviceWorkerPath declaration; the repo’s ESLint config extends semistandard, which enforces semicolons and will flag this.

Suggested change
const serviceWorkerPath = document.querySelector('meta[name="pwa-service-worker-path"]')?.content || '/service-worker.js'
const serviceWorkerPath = document.querySelector('meta[name="pwa-service-worker-path"]')?.content || '/service-worker.js';

Copilot uses AI. Check for mistakes.
Comment on lines 14 to 18
"/map",
"/beacon",
"/beacons",
"/triggers",
"/session",

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

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

Routes like map/beacon/beacons/triggers are under the /:event_name scope (e.g. /2026/map), but nonCacheableResources lists them as /map, /beacon, etc. With the new exact/startsWith("${path}/") check, the event-scoped URLs won’t match and will still be cached. Consider matching by suffix (e.g. endsWith('/map')) or generating the scoped paths explicitly.

Copilot uses AI. Check for mistakes.
"/auth"
"/auth",
"/icon.png",
"/apple-touch-icon",

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

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

nonCacheableResources includes /apple-touch-icon, but the conventional iOS icon URLs are /apple-touch-icon.png and /apple-touch-icon-precomposed.png. With the new pathname matcher, requests for the .png files won’t match this entry and may still be cached. Add the .png paths (or adjust matching) if the goal is to keep these non-cacheable.

Suggested change
"/apple-touch-icon",
"/apple-touch-icon",
"/apple-touch-icon.png",
"/apple-touch-icon-precomposed.png",

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +13
"/manifest",
"/service-worker",

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

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

The non-cacheable list includes /manifest and /service-worker, but this app uses /manifest.json and /service-worker.js (via format: :json/:js). With the new exact/startsWith("${path}/") matcher, those .json/.js requests won’t be treated as non-cacheable. Include the .json/.js variants or relax the matcher to cover them.

Suggested change
"/manifest",
"/service-worker",
"/manifest",
"/manifest.json",
"/service-worker",
"/service-worker.js",

Copilot uses AI. Check for mistakes.
Comment on lines 54 to 57
const fetchResponsePromise = fetch(request).then(async (networkResponse) => {
if (networkResponse.ok) {
const cache = await caches.open(cacheName);
cache.put(request, networkResponse.clone());
}

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

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

cacheFirstWithRefresh will attempt cache.put(request, …) for any request type. Cache.put only supports GET/HEAD; PATCH/POST requests (e.g., Turbo form submissions to routes not in nonCacheableResources) will throw and can break those requests. Add a guard to bypass caching for non-GET requests (or at least before calling cache.put).

Copilot uses AI. Check for mistakes.
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.

2 participants