Skip to content

Commit 68e3921

Browse files
authored
bug: fix issues with white labeling pwa (#456)
1 parent edcb38e commit 68e3921

4 files changed

Lines changed: 56 additions & 3 deletions

File tree

components/InstallPWA.vue

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,16 @@ export default {
143143
return false;
144144
}
145145
146+
// Already running as an installed app — don't offer install.
147+
// `$pwa.isPWAInstalled` only checks the display-mode media query, which
148+
// is unreliable on iOS, so also check navigator.standalone directly.
149+
if (
150+
(window.navigator as Navigator & { standalone?: boolean }).standalone ||
151+
window.matchMedia("(display-mode: standalone)").matches
152+
) {
153+
return false;
154+
}
155+
146156
if (this.isIOS) {
147157
return true;
148158
}

components/match/ReplayViewer.vue

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2548,14 +2548,28 @@ onMounted(() => {
25482548
if (dock) radarRO.observe(dock);
25492549
}
25502550
window.addEventListener("resize", recomputeRadarSize);
2551+
// Rotating a phone/tablet fires `orientationchange` BEFORE the viewport
2552+
// metrics (innerHeight / dvh / safe-area) have settled — iOS especially
2553+
// reports stale values for a moment — so re-fit a few times as it lands,
2554+
// otherwise the map height is wrong and the page scrolls (shoving the seek
2555+
// bar). `visualViewport` resize is the most reliable mobile signal.
2556+
window.addEventListener("orientationchange", recomputeAfterRotate);
2557+
window.visualViewport?.addEventListener("resize", recomputeRadarSize);
25512558
// Re-measure once after fonts/images settle so the initial top
25522559
// offset is right.
25532560
requestAnimationFrame(() => recomputeRadarSize());
25542561
});
2562+
function recomputeAfterRotate() {
2563+
recomputeRadarSize();
2564+
setTimeout(recomputeRadarSize, 200);
2565+
setTimeout(recomputeRadarSize, 450);
2566+
}
25552567
onUnmounted(() => {
25562568
radarRO?.disconnect();
25572569
if (typeof window !== "undefined") {
25582570
window.removeEventListener("resize", recomputeRadarSize);
2571+
window.removeEventListener("orientationchange", recomputeAfterRotate);
2572+
window.visualViewport?.removeEventListener("resize", recomputeRadarSize);
25592573
}
25602574
});
25612575

composables/useBranding.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,9 @@ export function useBranding() {
204204
manifest.rel = "manifest";
205205
document.head.appendChild(manifest);
206206
}
207-
manifest.href = `https://${apiDomain}/branding/manifest.webmanifest?v=${encodeURIComponent(version)}`;
208-
// CORS uses credentials:true (not "*"), so the fetch must send cookies.
209-
manifest.crossOrigin = "use-credentials";
207+
// Same-origin route (Nitro proxy) — a cross-origin manifest would
208+
// resolve start_url/scope to the API origin and break installability.
209+
manifest.href = `/branding/manifest.webmanifest?v=${encodeURIComponent(version)}`;
210210
}
211211
},
212212
{ immediate: true, deep: true },
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Same-origin PWA manifest for white-label instances.
2+
//
3+
// The web app manifest must be served from the SAME origin as the page,
4+
// otherwise its start_url/scope resolve to the API origin and the manifest
5+
// no longer applies to the document — Chrome never fires beforeinstallprompt
6+
// (no install button) and iOS won't reliably enter standalone mode.
7+
//
8+
// So we proxy the API's dynamic manifest (brand name + colors) through the
9+
// web origin. start_url ("/") resolves to the web origin here; the icon URLs
10+
// in the API manifest are absolute (API origin), and cross-origin icons are
11+
// allowed by the spec.
12+
13+
export default defineEventHandler(async (event) => {
14+
setResponseHeader(event, "Content-Type", "application/manifest+json");
15+
setResponseHeader(event, "Cache-Control", "public, max-age=60");
16+
17+
const apiDomain = process.env.NUXT_PUBLIC_API_DOMAIN;
18+
if (!apiDomain) {
19+
return sendRedirect(event, "/manifest.webmanifest", 302);
20+
}
21+
22+
try {
23+
return await $fetch(`https://${apiDomain}/branding/manifest.webmanifest`);
24+
} catch (err) {
25+
console.error("[branding-manifest] fetch failed:", err);
26+
// Fall back to the static build manifest rather than serving nothing.
27+
return sendRedirect(event, "/manifest.webmanifest", 302);
28+
}
29+
});

0 commit comments

Comments
 (0)