From 251f4ab790f2f4ac1eeddfd90d20021bb97c9138 Mon Sep 17 00:00:00 2001 From: Pamela Fox Date: Mon, 18 Aug 2025 10:25:32 -0700 Subject: [PATCH 1/3] frontend: Initialize MSAL before use, use stable instance, and guard async init (fixes uninitialized_public_client_application); show small loading state --- app/frontend/src/layoutWrapper.tsx | 82 ++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 26 deletions(-) diff --git a/app/frontend/src/layoutWrapper.tsx b/app/frontend/src/layoutWrapper.tsx index 7b00972458..524ed4d0df 100644 --- a/app/frontend/src/layoutWrapper.tsx +++ b/app/frontend/src/layoutWrapper.tsx @@ -1,6 +1,6 @@ -import { AccountInfo, EventType, PublicClientApplication } from "@azure/msal-browser"; +import { AuthenticationResult, EventType, PublicClientApplication } from "@azure/msal-browser"; import { checkLoggedIn, msalConfig, useLogin } from "./authConfig"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { MsalProvider } from "@azure/msal-react"; import { LoginContext } from "./loginContext"; import Layout from "./pages/layout/Layout"; @@ -8,38 +8,68 @@ import Layout from "./pages/layout/Layout"; const LayoutWrapper = () => { const [loggedIn, setLoggedIn] = useState(false); if (useLogin) { - var msalInstance = new PublicClientApplication(msalConfig); + // Create a stable MSAL instance (avoid re-init/duplicate listeners; single shared client for MsalProvider). + const msalInstance = useMemo(() => new PublicClientApplication(msalConfig), []); + const [initialized, setInitialized] = useState(false); + // Track mount state so we don't call setState after unmount if async init resolves late + const mounted = useRef(true); - // Default to using the first account if no account is active on page load - if (!msalInstance.getActiveAccount() && msalInstance.getAllAccounts().length > 0) { - // Account selection logic is app dependent. Adjust as needed for different use cases. - msalInstance.setActiveAccount(msalInstance.getActiveAccount()); - } + useEffect(() => { + // React StrictMode in development invokes effects twice (mount -> cleanup -> mount). + // Reset the flag here so this run is considered mounted. + mounted.current = true; + const init = async () => { + try { + await msalInstance.initialize(); - // Listen for sign-in event and set active account - msalInstance.addEventCallback(event => { - if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) { - const account = event.payload as AccountInfo; - msalInstance.setActiveAccount(account); - } - }); + // Default to using the first account if no account is active on page load + if (!msalInstance.getActiveAccount() && msalInstance.getAllAccounts().length > 0) { + msalInstance.setActiveAccount(msalInstance.getAllAccounts()[0]); + } - useEffect(() => { - const fetchLoggedIn = async () => { - setLoggedIn(await checkLoggedIn(msalInstance)); + // Listen for sign-in event and set active account + msalInstance.addEventCallback(event => { + if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) { + const result = event.payload as AuthenticationResult; + if (result.account) { + msalInstance.setActiveAccount(result.account); + } + } + }); + + if (mounted.current) { + try { + const isLoggedIn = await checkLoggedIn(msalInstance); + setLoggedIn(isLoggedIn); + } catch (e) { + // Swallow check error but still allow app to render + console.error("checkLoggedIn failed", e); + } + } + } catch (e) { + console.error("MSAL initialize failed", e); + } finally { + if (mounted.current) { + setInitialized(true); + } + } + }; + init(); + return () => { + // On unmount: flag as unmounted so any pending async in init() doesn't call setState + // This avoids React warnings about setting state on an unmounted component. + mounted.current = false; }; + }, [msalInstance]); - fetchLoggedIn(); - }, []); + if (!initialized) { + // Lightweight placeholder while MSAL initializes + return

Loading authentication…

; + } return ( - + From 5356dfe01ae468e4e15615c03026e17e2a63b49e Mon Sep 17 00:00:00 2001 From: Pamela Fox Date: Mon, 18 Aug 2025 10:51:29 -0700 Subject: [PATCH 2/3] frontend: inline catch for checkLoggedIn during auth init; keep initialize() gated with try/finally --- app/frontend/src/layoutWrapper.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/frontend/src/layoutWrapper.tsx b/app/frontend/src/layoutWrapper.tsx index 524ed4d0df..5c784fee6e 100644 --- a/app/frontend/src/layoutWrapper.tsx +++ b/app/frontend/src/layoutWrapper.tsx @@ -24,6 +24,7 @@ const LayoutWrapper = () => { // Default to using the first account if no account is active on page load if (!msalInstance.getActiveAccount() && msalInstance.getAllAccounts().length > 0) { + // Account selection logic is app dependent. Adjust as needed for different use cases. msalInstance.setActiveAccount(msalInstance.getAllAccounts()[0]); } @@ -38,13 +39,12 @@ const LayoutWrapper = () => { }); if (mounted.current) { - try { - const isLoggedIn = await checkLoggedIn(msalInstance); - setLoggedIn(isLoggedIn); - } catch (e) { + const isLoggedIn = await checkLoggedIn(msalInstance).catch(e => { // Swallow check error but still allow app to render console.error("checkLoggedIn failed", e); - } + return false; + }); + setLoggedIn(isLoggedIn); } } catch (e) { console.error("MSAL initialize failed", e); From f213e75b6621bc7b3d9c7e5fd8452995f658d916 Mon Sep 17 00:00:00 2001 From: Pamela Fox Date: Wed, 20 Aug 2025 09:52:48 -0700 Subject: [PATCH 3/3] Use different approach with MsalProvider in index --- .github/chatmodes/fixer.chatmode.md | 2 +- app/frontend/src/index.tsx | 63 +++++++++++++++++++---- app/frontend/src/layoutWrapper.tsx | 77 ++++++----------------------- 3 files changed, 71 insertions(+), 71 deletions(-) diff --git a/.github/chatmodes/fixer.chatmode.md b/.github/chatmodes/fixer.chatmode.md index 0e35b6450b..a5d052c8e2 100644 --- a/.github/chatmodes/fixer.chatmode.md +++ b/.github/chatmodes/fixer.chatmode.md @@ -24,4 +24,4 @@ You MUST check task output readiness before debugging, testing, or declaring wor - Frontend: Vite provides HMR; changes in the frontend are picked up automatically without restarting the task. - Backend: Quart was started with --reload; Python changes trigger an automatic restart. - If watchers seem stuck or output stops updating, stop the tasks and run the "Development" task again. -- To interact with a running application, use the Playwright MCP server +- To interact with a running application, use the Playwright MCP server. If testing login, you will need to navigate to 'localhost' instead of '127.0.0.1' since that's the URL allowed by the Entra application. diff --git a/app/frontend/src/index.tsx b/app/frontend/src/index.tsx index a8821c8c45..d77b08e9ac 100644 --- a/app/frontend/src/index.tsx +++ b/app/frontend/src/index.tsx @@ -4,12 +4,15 @@ import { createHashRouter, RouterProvider } from "react-router-dom"; import { I18nextProvider } from "react-i18next"; import { HelmetProvider } from "react-helmet-async"; import { initializeIcons } from "@fluentui/react"; +import { MsalProvider } from "@azure/msal-react"; +import { AuthenticationResult, EventType, PublicClientApplication } from "@azure/msal-browser"; import "./index.css"; import Chat from "./pages/chat/Chat"; import LayoutWrapper from "./layoutWrapper"; import i18next from "./i18n/config"; +import { msalConfig, useLogin } from "./authConfig"; initializeIcons(); @@ -34,12 +37,54 @@ const router = createHashRouter([ } ]); -ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - - - - - - -); +const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); + +// Bootstrap the app once; conditionally wrap with MsalProvider when login is enabled +(async () => { + let msalInstance: PublicClientApplication | undefined; + + if (useLogin) { + msalInstance = new PublicClientApplication(msalConfig); + try { + await msalInstance.initialize(); + + // Default active account to the first one if none is set + if (!msalInstance.getActiveAccount() && msalInstance.getAllAccounts().length > 0) { + msalInstance.setActiveAccount(msalInstance.getAllAccounts()[0]); + } + + // Keep active account in sync on login success + msalInstance.addEventCallback(event => { + if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) { + const result = event.payload as AuthenticationResult; + if (result.account) { + msalInstance!.setActiveAccount(result.account); + } + } + }); + } catch (e) { + // Non-fatal: render the app even if MSAL initialization fails + // eslint-disable-next-line no-console + console.error("MSAL initialize failed", e); + msalInstance = undefined; + } + } + + const appTree = ( + + + + {useLogin && msalInstance ? ( + + + + ) : ( + + )} + + + + ); + + root.render(appTree); +})(); diff --git a/app/frontend/src/layoutWrapper.tsx b/app/frontend/src/layoutWrapper.tsx index 5c784fee6e..c93e52acd4 100644 --- a/app/frontend/src/layoutWrapper.tsx +++ b/app/frontend/src/layoutWrapper.tsx @@ -1,78 +1,33 @@ -import { AuthenticationResult, EventType, PublicClientApplication } from "@azure/msal-browser"; -import { checkLoggedIn, msalConfig, useLogin } from "./authConfig"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { MsalProvider } from "@azure/msal-react"; +import { useEffect, useRef, useState } from "react"; +import { useMsal } from "@azure/msal-react"; +import { useLogin, checkLoggedIn } from "./authConfig"; import { LoginContext } from "./loginContext"; import Layout from "./pages/layout/Layout"; const LayoutWrapper = () => { const [loggedIn, setLoggedIn] = useState(false); if (useLogin) { - // Create a stable MSAL instance (avoid re-init/duplicate listeners; single shared client for MsalProvider). - const msalInstance = useMemo(() => new PublicClientApplication(msalConfig), []); - const [initialized, setInitialized] = useState(false); - // Track mount state so we don't call setState after unmount if async init resolves late + const { instance } = useMsal(); + // Keep track of the mounted state to avoid setting state in an unmounted component const mounted = useRef(true); - useEffect(() => { - // React StrictMode in development invokes effects twice (mount -> cleanup -> mount). - // Reset the flag here so this run is considered mounted. mounted.current = true; - const init = async () => { - try { - await msalInstance.initialize(); - - // Default to using the first account if no account is active on page load - if (!msalInstance.getActiveAccount() && msalInstance.getAllAccounts().length > 0) { - // Account selection logic is app dependent. Adjust as needed for different use cases. - msalInstance.setActiveAccount(msalInstance.getAllAccounts()[0]); - } - - // Listen for sign-in event and set active account - msalInstance.addEventCallback(event => { - if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) { - const result = event.payload as AuthenticationResult; - if (result.account) { - msalInstance.setActiveAccount(result.account); - } - } - }); - - if (mounted.current) { - const isLoggedIn = await checkLoggedIn(msalInstance).catch(e => { - // Swallow check error but still allow app to render - console.error("checkLoggedIn failed", e); - return false; - }); - setLoggedIn(isLoggedIn); - } - } catch (e) { - console.error("MSAL initialize failed", e); - } finally { - if (mounted.current) { - setInitialized(true); - } - } - }; - init(); + checkLoggedIn(instance) + .then(isLoggedIn => { + if (mounted.current) setLoggedIn(isLoggedIn); + }) + .catch(e => { + console.error("checkLoggedIn failed", e); + }); return () => { - // On unmount: flag as unmounted so any pending async in init() doesn't call setState - // This avoids React warnings about setting state on an unmounted component. mounted.current = false; }; - }, [msalInstance]); - - if (!initialized) { - // Lightweight placeholder while MSAL initializes - return

Loading authentication…

; - } + }, [instance]); return ( - - - - - + + + ); } else { return (