From 6b1b8bbc941bda6692a28d588fab672778719ae2 Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Wed, 19 Nov 2025 16:08:09 -0800 Subject: [PATCH] 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 --- .github/workflows/dev-build.yaml | 2 +- frontend/index.html | 7 +- frontend/src/App.jsx | 377 +++++++++--------- frontend/src/PWAContext.jsx | 93 +++++ .../ChatContainer/PromptInput/index.jsx | 6 +- frontend/src/index.css | 12 + frontend/tailwind.config.js | 1 + server/index.js | 15 +- server/utils/boot/MetaGenerator.js | 169 +++++++- 9 files changed, 480 insertions(+), 202 deletions(-) create mode 100644 frontend/src/PWAContext.jsx diff --git a/.github/workflows/dev-build.yaml b/.github/workflows/dev-build.yaml index 7bc59546..8d1f8d58 100644 --- a/.github/workflows/dev-build.yaml +++ b/.github/workflows/dev-build.yaml @@ -6,7 +6,7 @@ concurrency: on: push: - branches: ['4534-disable-prisma-telemetry'] # put your current branch to create a build. Core team only. + branches: ['4595-refactor-pwa'] # put your current branch to create a build. Core team only. paths-ignore: - '**.md' - 'cloud-deployments/*' diff --git a/frontend/index.html b/frontend/index.html index 22cc5b0f..6eb5b43c 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -28,6 +28,11 @@ + + + + + @@ -35,4 +40,4 @@ - \ No newline at end of file + diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 5ae91578..83d2b301 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -17,6 +17,7 @@ import { PfpProvider } from "./PfpContext"; import { LogoProvider } from "./LogoContext"; import { FullScreenLoader } from "./components/Preloader"; import { ThemeProvider } from "./ThemeContext"; +import { PWAModeProvider } from "./PWAContext"; import KeyboardShortcutsHelp from "@/components/KeyboardShortcutsHelp"; const Main = lazy(() => import("@/pages/Main")); @@ -96,190 +97,208 @@ const MobileConnections = lazy( export default function App() { return ( - }> - - - - - - } /> - } /> - } - /> + + }> + + + + + + } + /> + } /> + } + /> - } - /> - } - /> - } - /> - } /> + } + /> + } + /> + } + /> + } + /> - {/* Admin */} - } - /> - - } - /> - } - /> - - } - /> - - } - /> - } - /> - } - /> - - } - /> - - } - /> - } - /> - } - /> - {/* Manager */} - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - - } - /> - } - /> - } - /> - } - /> - } - /> - {/* Onboarding Flow */} - } /> - } - /> + {/* Admin */} + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + } + /> + } + /> + + } + /> + + } + /> + } + /> + } + /> + {/* Manager */} + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + + } + /> + } + /> + } + /> + } + /> + } + /> + {/* Onboarding Flow */} + } /> + } + /> - {/* Experimental feature pages */} - {/* Live Document Sync feature */} - } - /> + {/* Experimental feature pages */} + {/* Live Document Sync feature */} + + } + /> - } - /> - - } - /> - } - /> + } + /> + + } + /> + + } + /> - } - /> - - - - - - - - + } + /> + + + + + + + + + ); } diff --git a/frontend/src/PWAContext.jsx b/frontend/src/PWAContext.jsx new file mode 100644 index 00000000..6495a698 --- /dev/null +++ b/frontend/src/PWAContext.jsx @@ -0,0 +1,93 @@ +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 ( + {children} + ); +} + +export function usePWAMode() { + return useContext(PWAModeContext); +} diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx index 8a498f0b..b8eca495 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx @@ -243,7 +243,7 @@ export default function PromptInput({ } return ( -
+
-
+