merlyn/frontend/src/PWAContext.jsx
Timothy Carambat 6b1b8bbc94
4595 refactor PWA (#4664)
* feat: add web app manifest and mobile PWA meta tags

* feat: serve dynamic manifest.json with custom branding for pwa

* feat: add ios status bar theming for pwa

* fix: prevent overscroll behavior for mobile

* fix: prevent ios safari auto-zoom on chat input

* fix: remove theme-color meta tags conflicting with ios status bar

* fix: add missing apple-mobile-web-app-capable meta tag for ios pwa

* fix: move catch-all route after manifest endpoint to prevent interception

* feat: add pwa detection helper and conditional styling for standalone mode

* PWA refactor

* undo changes to native CSS

* class fix

* proper response obj

* fix patch for import

* fix manifest errors

---------

Co-authored-by: Christian De Santis <christian.constantino98@gmail.com>
2025-11-19 16:08:09 -08:00

94 lines
2.5 KiB
JavaScript

import React, {
createContext,
useContext,
useEffect,
useMemo,
useState,
} from "react";
/**
* Detects if the application is running as a standalone PWA
* @returns {boolean} True if running as standalone PWA
*/
function isStandalonePWA() {
if (typeof window === "undefined") return false;
const matchesStandaloneDisplayMode =
typeof window.matchMedia === "function"
? window.matchMedia("(display-mode: standalone)")?.matches
: false;
const isIOSStandalone = window.navigator?.standalone === true; // iOS Safari
const androidReferrer =
typeof document !== "undefined" && document?.referrer
? document.referrer.includes("android-app://")
: false;
return Boolean(
matchesStandaloneDisplayMode || isIOSStandalone || androidReferrer
);
}
const PWAModeContext = createContext({ isPWA: false });
export function PWAModeProvider({ children }) {
const [isPWA, setIsPWA] = useState(() => isStandalonePWA());
useEffect(() => {
if (typeof window === "undefined") return undefined;
const mediaQuery =
typeof window.matchMedia === "function"
? window.matchMedia("(display-mode: standalone)")
: null;
const updateStatus = () => setIsPWA(isStandalonePWA());
updateStatus();
if (mediaQuery?.addEventListener) {
mediaQuery.addEventListener("change", updateStatus);
} else if (mediaQuery?.addListener) {
mediaQuery.addListener(updateStatus);
}
window.addEventListener("appinstalled", updateStatus);
window.addEventListener("visibilitychange", updateStatus);
return () => {
if (mediaQuery?.removeEventListener) {
mediaQuery.removeEventListener("change", updateStatus);
} else if (mediaQuery?.removeListener) {
mediaQuery.removeListener(updateStatus);
}
window.removeEventListener("appinstalled", updateStatus);
window.removeEventListener("visibilitychange", updateStatus);
};
}, []);
useEffect(() => {
if (typeof document === "undefined") return undefined;
document.body.classList.toggle("pwa", isPWA);
document.documentElement?.setAttribute(
"data-pwa",
isPWA ? "true" : "false"
);
return () => {
document.body.classList.remove("pwa");
document.documentElement?.removeAttribute("data-pwa");
};
}, [isPWA]);
const value = useMemo(() => ({ isPWA }), [isPWA]);
return (
<PWAModeContext.Provider value={value}>{children}</PWAModeContext.Provider>
);
}
export function usePWAMode() {
return useContext(PWAModeContext);
}