+ )
+};
\ No newline at end of file
diff --git a/samples/msal-react-samples/react16-sample/src/ui-components/WelcomeName.jsx b/samples/msal-react-samples/react16-sample/src/ui-components/WelcomeName.jsx
new file mode 100644
index 0000000000..4f135587a0
--- /dev/null
+++ b/samples/msal-react-samples/react16-sample/src/ui-components/WelcomeName.jsx
@@ -0,0 +1,47 @@
+import { useEffect, useState, useCallback } from "react";
+import { useMsal } from "@azure/msal-react";
+import { EventType } from "@azure/msal-browser";
+import Typography from "@material-ui/core/Typography";
+
+const WelcomeName = () => {
+ const { instance } = useMsal();
+ const [name, setName] = useState(null);
+
+ const updateName = useCallback(() => {
+ const activeAccount = instance.getActiveAccount();
+ if (activeAccount) {
+ setName(activeAccount.name.split(' ')[0]);
+ } else {
+ setName(null);
+ }
+ }, [instance]);
+
+ useEffect(() => {
+ // Set the name from the current active account on mount
+ updateName();
+
+ // Subscribe to active account changes so the component updates when
+ // setActiveAccount is called. This avoids the React 16/17 batching issue where the
+ // render triggered by ACQUIRE_TOKEN_SUCCESS runs before setActiveAccount
+ // has been called, causing getActiveAccount() to return null.
+ const callbackId = instance.addEventCallback((event) => {
+ if (event.eventType === EventType.ACTIVE_ACCOUNT_CHANGED) {
+ updateName();
+ }
+ });
+
+ return () => {
+ if (callbackId) {
+ instance.removeEventCallback(callbackId);
+ }
+ };
+ }, [instance, updateName]);
+
+ if (name) {
+ return Welcome, {name};
+ } else {
+ return null;
+ }
+};
+
+export default WelcomeName;
\ No newline at end of file
diff --git a/samples/msal-react-samples/react16-sample/src/utils/MsGraphApiCall.js b/samples/msal-react-samples/react16-sample/src/utils/MsGraphApiCall.js
new file mode 100644
index 0000000000..360493a86b
--- /dev/null
+++ b/samples/msal-react-samples/react16-sample/src/utils/MsGraphApiCall.js
@@ -0,0 +1,33 @@
+import { loginRequest, graphConfig } from "../authConfig";
+import { msalInstance } from "../index";
+
+export async function callMsGraph(accessToken) {
+ if (!accessToken) {
+ const account = msalInstance.getActiveAccount();
+ if (!account) {
+ throw Error("No active account! Verify a user has been signed in and setActiveAccount has been called.");
+ }
+
+ const response = await msalInstance.acquireTokenSilent({
+ ...loginRequest,
+ account: account
+ });
+ accessToken = response.accessToken;
+ }
+
+ const headers = new Headers();
+ const bearer = `Bearer ${accessToken}`;
+
+ headers.append("Authorization", bearer);
+
+ const options = {
+ method: "GET",
+ headers: headers
+ };
+
+ const response = await fetch(graphConfig.graphMeEndpoint, options);
+ if (!response.ok) {
+ throw new Error(`MS Graph request failed: ${response.status} ${response.statusText}`);
+ }
+ return response.json();
+}
diff --git a/samples/msal-react-samples/react16-sample/src/utils/NavigationClient.js b/samples/msal-react-samples/react16-sample/src/utils/NavigationClient.js
new file mode 100644
index 0000000000..70eab8c6d7
--- /dev/null
+++ b/samples/msal-react-samples/react16-sample/src/utils/NavigationClient.js
@@ -0,0 +1,28 @@
+import { NavigationClient } from "@azure/msal-browser";
+
+/**
+ * This is an example for overriding the default function MSAL uses to navigate to other urls in your webpage
+ */
+export class CustomNavigationClient extends NavigationClient {
+ constructor(navigate) {
+ super();
+ this.navigate = navigate;
+ }
+
+ /**
+ * Navigates to other pages within the same web application
+ * You can use the useNavigate hook provided by react-router-dom to take advantage of client-side routing
+ * @param url
+ * @param options
+ */
+ async navigateInternal(url, options) {
+ const relativePath = url.replace(window.location.origin, "");
+ if (options.noHistory) {
+ this.navigate(relativePath, { replace: true });
+ } else {
+ this.navigate(relativePath);
+ }
+
+ return false;
+ }
+}
diff --git a/samples/msal-react-samples/react16-sample/test/home.spec.ts b/samples/msal-react-samples/react16-sample/test/home.spec.ts
new file mode 100644
index 0000000000..eb2c005709
--- /dev/null
+++ b/samples/msal-react-samples/react16-sample/test/home.spec.ts
@@ -0,0 +1,148 @@
+import * as puppeteer from "puppeteer";
+import {
+ Screenshot,
+ setupCredentials,
+ enterCredentials,
+ RETRY_TIMES,
+ LabClient,
+ LabApiQueryParams,
+ AzureEnvironments,
+ AppTypes,
+ BrowserCacheUtils,
+} from "e2e-test-utils";
+
+const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/home-tests`;
+
+describe("/ (Home Page)", () => {
+ jest.retryTimes(RETRY_TIMES);
+ let browser: puppeteer.Browser;
+ let context: puppeteer.BrowserContext;
+ let page: puppeteer.Page;
+ let port: number;
+ let username: string;
+ let accountPwd: string;
+ let BrowserCache: BrowserCacheUtils;
+
+ beforeAll(async () => {
+ // @ts-ignore
+ browser = await global.__BROWSER__;
+ // @ts-ignore
+ port = global.__PORT__;
+
+ const labApiParams: LabApiQueryParams = {
+ azureEnvironment: AzureEnvironments.CLOUD,
+ appType: AppTypes.CLOUD,
+ };
+
+ const labClient = new LabClient();
+ const envResponse = await labClient.getVarsByCloudEnvironment(
+ labApiParams
+ );
+
+ [username, accountPwd] = await setupCredentials(
+ envResponse[0],
+ labClient
+ );
+ });
+
+ beforeEach(async () => {
+ context = await browser.createBrowserContext();
+ page = await context.newPage();
+ page.setDefaultTimeout(5000);
+ BrowserCache = new BrowserCacheUtils(page, "localStorage");
+ await page.goto(`http://localhost:${port}`);
+ });
+
+ afterEach(async () => {
+ await page.close();
+ await context.close();
+ });
+
+ it("AuthenticatedTemplate - children are rendered after logging in with loginRedirect", async () => {
+ const testName = "redirectBaseCase";
+ const screenshot = new Screenshot(
+ `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}`
+ );
+ await screenshot.takeScreenshot(page, "Page loaded");
+
+ // Initiate Login
+ const signInButton = await page.waitForSelector(
+ "xpath=//button[contains(., 'Login')]"
+ );
+ await signInButton.click();
+ await screenshot.takeScreenshot(page, "Login button clicked");
+ const loginRedirectButton = await page.waitForSelector(
+ "xpath=//li[contains(., 'Sign in using Redirect')]"
+ );
+ await loginRedirectButton.click();
+
+ await enterCredentials(page, screenshot, username, accountPwd);
+ await screenshot.takeScreenshot(page, "Returned to app");
+
+ // Verify UI now displays logged in content
+ await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]");
+ const profileButton = await page.waitForSelector(
+ "xpath=//header//button"
+ );
+ await profileButton.click();
+ const logoutButtons = await page.$$(
+ "xpath/.//li[contains(., 'Logout using')]"
+ );
+ expect(logoutButtons.length).toBe(2);
+ await screenshot.takeScreenshot(page, "App signed in");
+
+ // Verify tokens are in cache
+ await BrowserCache.verifyTokenStore({
+ scopes: ['User.Read'],
+ });
+ });
+
+ it("AuthenticatedTemplate - children are rendered after logging in with loginPopup", async () => {
+ const testName = "popupBaseCase";
+ const screenshot = new Screenshot(
+ `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}`
+ );
+ await screenshot.takeScreenshot(page, "Page loaded");
+
+ // Initiate Login
+ const signInButton = await page.waitForSelector(
+ "xpath=//button[contains(., 'Login')]"
+ );
+ await signInButton.click();
+ await screenshot.takeScreenshot(page, "Login button clicked");
+ const loginPopupButton = await page.waitForSelector(
+ "xpath=//li[contains(., 'Sign in using Popup')]"
+ );
+ const newPopupWindowPromise = new Promise((resolve) =>
+ page.once("popup", resolve)
+ );
+ await loginPopupButton.click();
+ const popupPage = await newPopupWindowPromise;
+ if (!popupPage) {
+ throw new Error('Popup window was not opened');
+ }
+
+ await enterCredentials(popupPage, screenshot, username, accountPwd);
+ await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]", {
+ timeout: 3000,
+ });
+ await screenshot.takeScreenshot(page, "Popup closed");
+
+ // Verify UI now displays logged in content
+ await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]");
+ const profileButton = await page.waitForSelector(
+ "xpath=//header//button"
+ );
+ await profileButton.click();
+ const logoutButtons = await page.$$(
+ "xpath/.//li[contains(., 'Logout using')]"
+ );
+ expect(logoutButtons.length).toBe(2);
+ await screenshot.takeScreenshot(page, "App signed in");
+
+ // Verify tokens are in cache
+ await BrowserCache.verifyTokenStore({
+ scopes: ['User.Read'],
+ });
+ });
+});
diff --git a/samples/msal-react-samples/react16-sample/test/profile.spec.ts b/samples/msal-react-samples/react16-sample/test/profile.spec.ts
new file mode 100644
index 0000000000..e3b383cf66
--- /dev/null
+++ b/samples/msal-react-samples/react16-sample/test/profile.spec.ts
@@ -0,0 +1,170 @@
+import * as puppeteer from "puppeteer";
+import {
+ Screenshot,
+ setupCredentials,
+ enterCredentials,
+ RETRY_TIMES,
+ LabClient,
+ LabApiQueryParams,
+ AzureEnvironments,
+ AppTypes,
+ BrowserCacheUtils,
+} from "e2e-test-utils";
+
+const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/profile-tests`;
+
+async function verifyTokenStore(
+ BrowserCache: BrowserCacheUtils,
+ scopes: string[]
+): Promise {
+ await BrowserCache.verifyTokenStore({
+ scopes,
+ });
+ const telemetryCacheEntry = await BrowserCache.getTelemetryCacheEntry(
+ "b5c2e510-4a17-4feb-b219-e55aa5b74144"
+ );
+ expect(telemetryCacheEntry).not.toBeNull();
+ expect(telemetryCacheEntry["cacheHits"]).toBeGreaterThanOrEqual(1);
+}
+
+describe("/profile", () => {
+ jest.retryTimes(RETRY_TIMES);
+ let browser: puppeteer.Browser;
+ let context: puppeteer.BrowserContext;
+ let page: puppeteer.Page;
+ let port: number;
+ let username: string;
+ let accountPwd: string;
+ let BrowserCache: BrowserCacheUtils;
+
+ beforeAll(async () => {
+ // @ts-ignore
+ browser = await global.__BROWSER__;
+ // @ts-ignore
+ port = global.__PORT__;
+
+ const labApiParams: LabApiQueryParams = {
+ azureEnvironment: AzureEnvironments.CLOUD,
+ appType: AppTypes.CLOUD,
+ };
+
+ const labClient = new LabClient();
+ const envResponse = await labClient.getVarsByCloudEnvironment(
+ labApiParams
+ );
+
+ [username, accountPwd] = await setupCredentials(
+ envResponse[0],
+ labClient
+ );
+ });
+
+ beforeEach(async () => {
+ context = await browser.createBrowserContext();
+ page = await context.newPage();
+ page.setDefaultTimeout(5000);
+ BrowserCache = new BrowserCacheUtils(page, "localStorage");
+ await page.goto(`http://localhost:${port}`);
+ });
+
+ afterEach(async () => {
+ await page.close();
+ await context.close();
+ });
+
+ it("MsalAuthenticationTemplate - invokes loginPopup if user is not signed in", async () => {
+ const testName = "MsalAuthenticationTemplateBaseCase";
+ const screenshot = new Screenshot(
+ `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}`
+ );
+ await screenshot.takeScreenshot(page, "Home page loaded");
+
+ // Navigate to /profile and expect popup to be opened without interaction
+ const newPopupWindowPromise = new Promise((resolve) =>
+ page.once("popup", resolve)
+ );
+ await page.goto(`http://localhost:${port}/profile`);
+ await screenshot.takeScreenshot(page, "Profile page loaded");
+ const popupPage = await newPopupWindowPromise;
+ if (!popupPage) {
+ throw new Error('Popup window was not opened');
+ }
+
+ await enterCredentials(popupPage, screenshot, username, accountPwd);
+
+ // Wait for Graph data to display
+ await page.waitForSelector("xpath/.//div/ul/li[contains(., 'Name')]", {
+ timeout: 5000,
+ });
+ await screenshot.takeScreenshot(page, "Graph data acquired");
+
+ // Verify UI now displays logged in content
+ await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]");
+ const profileButton = await page.waitForSelector(
+ "xpath=//header//button"
+ );
+ await profileButton.click();
+ const logoutButtons = await page.$$(
+ "xpath/.//li[contains(., 'Logout using')]"
+ );
+ expect(logoutButtons.length).toBe(2);
+ await screenshot.takeScreenshot(page, "App signed in");
+
+ // Verify tokens are in cache
+ await verifyTokenStore(BrowserCache, ["User.Read"]);
+ });
+
+ it("MsalAuthenticationTemplate - renders children without invoking login if user is already signed in", async () => {
+ const testName = "MsalAuthenticationTemplateSignedInCase";
+ const screenshot = new Screenshot(
+ `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}`
+ );
+ await screenshot.takeScreenshot(page, "Page loaded");
+
+ // Initiate Login
+ const signInButton = await page.waitForSelector(
+ "xpath=//button[contains(., 'Login')]"
+ );
+ await signInButton.click();
+ await screenshot.takeScreenshot(page, "Login button clicked");
+ const loginPopupButton = await page.waitForSelector(
+ "xpath=//li[contains(., 'Sign in using Popup')]"
+ );
+ const newPopupWindowPromise = new Promise((resolve) =>
+ page.once("popup", resolve)
+ );
+ await loginPopupButton.click();
+ const popupPage = await newPopupWindowPromise;
+ if (!popupPage) {
+ throw new Error('Popup window was not opened');
+ }
+
+ await enterCredentials(popupPage, screenshot, username, accountPwd);
+ await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]", {
+ timeout: 3000,
+ });
+ await screenshot.takeScreenshot(page, "Popup closed");
+
+ // Verify UI now displays logged in content
+ await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]");
+ const profileButton = await page.waitForSelector(
+ "xpath=//header//button"
+ );
+ await profileButton.click();
+ const logoutButtons = await page.$$(
+ "xpath/.//li[contains(., 'Logout using')]"
+ );
+ expect(logoutButtons.length).toBe(2);
+ await screenshot.takeScreenshot(page, "App signed in");
+
+ // Go to protected page
+ await page.goto(`http://localhost:${port}/profile`);
+ // Wait for Graph data to display
+ await page.waitForSelector("xpath/.//div/ul/li[contains(., 'Name')]", {
+ timeout: 5000,
+ });
+ await screenshot.takeScreenshot(page, "Graph data acquired");
+ // Verify tokens are in cache
+ await verifyTokenStore(BrowserCache, ["User.Read"]);
+ });
+});
diff --git a/samples/msal-react-samples/react16-sample/test/profileRawContext.spec.ts b/samples/msal-react-samples/react16-sample/test/profileRawContext.spec.ts
new file mode 100644
index 0000000000..ff4df3c2a2
--- /dev/null
+++ b/samples/msal-react-samples/react16-sample/test/profileRawContext.spec.ts
@@ -0,0 +1,104 @@
+import * as puppeteer from "puppeteer";
+import {
+ Screenshot,
+ setupCredentials,
+ enterCredentials,
+ RETRY_TIMES,
+ LabClient,
+ LabApiQueryParams,
+ AzureEnvironments,
+ AppTypes,
+ BrowserCacheUtils,
+} from "e2e-test-utils";
+
+const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/profileRawContext-tests`;
+
+async function verifyTokenStore(
+ BrowserCache: BrowserCacheUtils,
+ scopes: string[]
+): Promise {
+ await BrowserCache.verifyTokenStore({
+ scopes,
+ });
+ const telemetryCacheEntry = await BrowserCache.getTelemetryCacheEntry(
+ "b5c2e510-4a17-4feb-b219-e55aa5b74144"
+ );
+ expect(telemetryCacheEntry).not.toBeNull();
+ expect(telemetryCacheEntry["cacheHits"]).toBeGreaterThanOrEqual(1);
+}
+
+describe("/profileRawContext", () => {
+ jest.retryTimes(RETRY_TIMES);
+ let browser: puppeteer.Browser;
+ let context: puppeteer.BrowserContext;
+ let page: puppeteer.Page;
+ let port: number;
+ let username: string;
+ let accountPwd: string;
+ let BrowserCache: BrowserCacheUtils;
+
+ beforeAll(async () => {
+ // @ts-ignore
+ browser = await global.__BROWSER__;
+ // @ts-ignore
+ port = global.__PORT__;
+
+ const labApiParams: LabApiQueryParams = {
+ azureEnvironment: AzureEnvironments.CLOUD,
+ appType: AppTypes.CLOUD,
+ };
+
+ const labClient = new LabClient();
+ const envResponse = await labClient.getVarsByCloudEnvironment(
+ labApiParams
+ );
+
+ [username, accountPwd] = await setupCredentials(
+ envResponse[0],
+ labClient
+ );
+ });
+
+ beforeEach(async () => {
+ context = await browser.createBrowserContext();
+ page = await context.newPage();
+ page.setDefaultTimeout(5000);
+ BrowserCache = new BrowserCacheUtils(page, "localStorage");
+ await page.goto(`http://localhost:${port}`);
+ });
+
+ afterEach(async () => {
+ await page.close();
+ await context.close();
+ });
+
+ it("MsalAuthenticationTemplate - invokes loginPopup if user is not signed in (class component w/ raw context)", async () => {
+ const testName = "MsalAuthenticationTemplatePopupCase";
+ const screenshot = new Screenshot(
+ `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}`
+ );
+ await screenshot.takeScreenshot(page, "Home page loaded");
+
+ // Navigate to /profile and expect popup to be opened without interaction
+ const newPopupWindowPromise = new Promise((resolve) =>
+ page.once("popup", resolve)
+ );
+ await page.goto(`http://localhost:${port}/profileRawContext`);
+ await screenshot.takeScreenshot(page, "Profile page loaded");
+ const popupPage = await newPopupWindowPromise;
+ if (!popupPage) {
+ throw new Error('Popup window was not opened');
+ }
+
+ await enterCredentials(popupPage, screenshot, username, accountPwd);
+
+ // Wait for Graph data to display
+ await page.waitForSelector("xpath/.//div/ul/li[contains(., 'Name')]", {
+ timeout: 5000,
+ });
+ await screenshot.takeScreenshot(page, "Graph data acquired");
+
+ // Verify tokens are in cache
+ await verifyTokenStore(BrowserCache, ["User.Read"]);
+ });
+});
diff --git a/samples/msal-react-samples/react16-sample/test/profileWithMsal.spec.ts b/samples/msal-react-samples/react16-sample/test/profileWithMsal.spec.ts
new file mode 100644
index 0000000000..1106546587
--- /dev/null
+++ b/samples/msal-react-samples/react16-sample/test/profileWithMsal.spec.ts
@@ -0,0 +1,97 @@
+import * as puppeteer from "puppeteer";
+import {
+ Screenshot,
+ setupCredentials,
+ enterCredentials,
+ RETRY_TIMES,
+ LabClient,
+ LabApiQueryParams,
+ AzureEnvironments,
+ AppTypes,
+ BrowserCacheUtils,
+} from "e2e-test-utils";
+
+const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/profileWithMsal-tests`;
+
+async function verifyTokenStore(
+ BrowserCache: BrowserCacheUtils,
+ scopes: string[]
+): Promise {
+ await BrowserCache.verifyTokenStore({
+ scopes,
+ });
+ const telemetryCacheEntry = await BrowserCache.getTelemetryCacheEntry(
+ "b5c2e510-4a17-4feb-b219-e55aa5b74144"
+ );
+ expect(telemetryCacheEntry).not.toBeNull();
+ expect(telemetryCacheEntry["cacheHits"]).toBeGreaterThanOrEqual(1);
+}
+
+describe("/profileWithMsal", () => {
+ jest.retryTimes(RETRY_TIMES);
+ let browser: puppeteer.Browser;
+ let context: puppeteer.BrowserContext;
+ let page: puppeteer.Page;
+ let port: number;
+ let username: string;
+ let accountPwd: string;
+ let BrowserCache: BrowserCacheUtils;
+
+ beforeAll(async () => {
+ // @ts-ignore
+ browser = await global.__BROWSER__;
+ // @ts-ignore
+ port = global.__PORT__;
+
+ const labApiParams: LabApiQueryParams = {
+ azureEnvironment: AzureEnvironments.CLOUD,
+ appType: AppTypes.CLOUD,
+ };
+
+ const labClient = new LabClient();
+ const envResponse = await labClient.getVarsByCloudEnvironment(
+ labApiParams
+ );
+
+ [username, accountPwd] = await setupCredentials(
+ envResponse[0],
+ labClient
+ );
+ });
+
+ beforeEach(async () => {
+ context = await browser.createBrowserContext();
+ page = await context.newPage();
+ page.setDefaultTimeout(5000);
+ BrowserCache = new BrowserCacheUtils(page, "localStorage");
+ await page.goto(`http://localhost:${port}`);
+ });
+
+ afterEach(async () => {
+ await page.close();
+ await context.close();
+ });
+
+ it("MsalAuthenticationTemplate - invokes loginRedirect if user is not signed in (class component w/ withMsal HOC)", async () => {
+ const testName = "MsalAuthenticationTemplateRedirectCase";
+ const screenshot = new Screenshot(
+ `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}`
+ );
+ await screenshot.takeScreenshot(page, "Home page loaded");
+
+ // Navigate to /profileWithMsal and expect redirect to be initiated without interaction
+ await page.goto(`http://localhost:${port}/profileWithMsal`);
+ await screenshot.takeScreenshot(page, "Profile page loaded");
+
+ await enterCredentials(page, screenshot, username, accountPwd);
+
+ // Wait for Graph data to display
+ await page.waitForSelector("xpath/.//div/ul/li[contains(., 'Name')]", {
+ timeout: 5000,
+ });
+ await screenshot.takeScreenshot(page, "Graph data acquired");
+
+ // Verify tokens are in cache
+ await verifyTokenStore(BrowserCache, ["User.Read"]);
+ });
+});
diff --git a/samples/msal-react-samples/react16-sample/tsconfig.json b/samples/msal-react-samples/react16-sample/tsconfig.json
new file mode 100644
index 0000000000..a8df22f95c
--- /dev/null
+++ b/samples/msal-react-samples/react16-sample/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../tsconfig.json"
+}
\ No newline at end of file
diff --git a/samples/msal-react-samples/react16-sample/vite.config.js b/samples/msal-react-samples/react16-sample/vite.config.js
new file mode 100644
index 0000000000..9be5c7392c
--- /dev/null
+++ b/samples/msal-react-samples/react16-sample/vite.config.js
@@ -0,0 +1,10 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 3000,
+ strictPort: true,
+ },
+});
diff --git a/samples/msal-react-samples/react17-sample/.env.development b/samples/msal-react-samples/react17-sample/.env.development
new file mode 100644
index 0000000000..b8e1347305
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/.env.development
@@ -0,0 +1,5 @@
+BROWSER=none
+
+VITE_CLIENT_ID=ENTER_CLIENT_ID_HERE
+VITE_AUTHORITY=https://login.microsoftonline.com/ENTER_TENANT_ID_HERE
+VITE_REDIRECT_URI=/redirect
diff --git a/samples/msal-react-samples/react17-sample/.env.e2e b/samples/msal-react-samples/react17-sample/.env.e2e
new file mode 100644
index 0000000000..7b1d8d02ec
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/.env.e2e
@@ -0,0 +1,5 @@
+BROWSER=none
+
+VITE_CLIENT_ID=b5c2e510-4a17-4feb-b219-e55aa5b74144
+VITE_AUTHORITY=https://login.microsoftonline.com/common
+VITE_REDIRECT_URI=/redirect
diff --git a/samples/msal-react-samples/react17-sample/.gitignore b/samples/msal-react-samples/react17-sample/.gitignore
new file mode 100644
index 0000000000..227a007b62
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/.gitignore
@@ -0,0 +1,20 @@
+# dependencies
+/node_modules
+
+# testing
+/coverage
+
+# production
+/build
+/dist
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
diff --git a/samples/msal-react-samples/react17-sample/.npmrc b/samples/msal-react-samples/react17-sample/.npmrc
new file mode 100644
index 0000000000..43c97e719a
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/.npmrc
@@ -0,0 +1 @@
+package-lock=false
diff --git a/samples/msal-react-samples/react17-sample/README.md b/samples/msal-react-samples/react17-sample/README.md
new file mode 100644
index 0000000000..112839a3ab
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/README.md
@@ -0,0 +1,87 @@
+# MSAL.js for React Sample - React 17 Compatibility
+
+## About this sample
+
+This sample is derived from the [react-router-sample](../react-router-sample) and demonstrates MSAL React running with **React 17**. It uses `ReactDOM.render` (React 16/17 API) for rendering and MUI v5 (`@mui/material`) for UI components.
+
+## Notable files and what they demonstrate
+
+1. `./src/App.jsx` - Shows implementation of `MsalProvider`, all children will have access to `@azure/msal-react` context, hooks and components.
+1. `./src/index.jsx` - Shows initialization of the `PublicClientApplication` that is passed to `App.jsx`
+1. `./src/pages/Home.jsx` - Homepage, shows how to conditionally render content using `AuthenticatedTemplate` and `UnauthenticatedTemplate` depending on whether or not a user is signed in.
+1. `./src/pages/Profile.jsx` - Example of a protected route using `MsalAuthenticationTemplate`. If a user is not yet signed in, signin will be invoked automatically. If a user is signed in it will acquire an access token and make a call to MS Graph to fetch user profile data.
+1. `./src/authConfig.js` - Configuration options for `PublicClientApplication` and token requests.
+1. `./src/ui-components/SignInSignOutButton.jsx` - Example of how to conditionally render a Sign In or Sign Out button using the `useIsAuthenticated` hook.
+1. `./src/ui-components/SignInButton.jsx` - Example of how to get the `PublicClientApplication` instance using the `useMsal` hook and invoking a login function.
+1. `./src/ui-components/SignOutButton.jsx` - Example of how to get the `PublicClientApplication` instance using the `useMsal` hook and invoking a logout function.
+1. `./src/utils/MsGraphApiCall.js` - Example of how to call the MS Graph API with an access token.
+1. `./src/utils/NavigationClient.js` - Example implementation of `INavigationClient` which can be used to override the default navigation functions MSAL.js uses
+
+### (Optional) MSAL React and class components
+
+For a demonstration of how to use MSAL React with class components, see: `./src/pages/ProfileWithMsal.jsx` and `./src/pages/ProfileRawContext.jsx`.
+
+*After* you initialize `MsalProvider`, there are 3 approaches you can take to protect your class components with MSAL React:
+
+1. Wrap each component that you want to protect with `withMsal` higher-order component (HOC) (e.g. [Profile](./src/pages/ProfileWithMsal.jsx#Profile)).
+1. Consume the raw context directly (e.g. [ProfileContent](./src/pages/ProfileRawContext.jsx#ProfileContent)).
+1. Pass context down from a parent component that has access to the `msalContext` via one of the other means above (e.g. [ProfileContent](./src/pages/ProfileWithMsal.jsx#ProfileContent)).
+
+For more information, visit:
+
+- [Docs: Class Components](../../../lib/msal-react/docs/class-components.md)
+- [MSAL React FAQ](../../../lib/msal-react/FAQ.md)
+
+## How to run the sample
+
+### Pre-requisites
+
+- Ensure [all pre-requisites](../../../lib/msal-react/README.md#prerequisites) have been completed to run `@azure/msal-react`.
+- Install node.js if needed ().
+
+### Configure the application
+
+- Open `./.env.development` in an editor.
+- Replace `ENTER_CLIENT_ID_HERE` with the Application (client) ID from the portal registration, or use the currently configured lab registration.
+- Replace `ENTER_TENANT_ID_HERE` with the tenant ID from the portal registration, or use the currently configured lab registration.
+ - Optionally, you may replace any of the other parameters, or you can remove them and use the default values.
+
+These parameters are used in `./src/authConfig.js` to configure MSAL.
+
+#### Install npm dependencies for sample
+
+```bash
+# Install dev dependencies for msal-react and msal-browser from root of repo
+npm install
+
+# Change directory to sample directory
+cd samples/msal-react-samples/react17-sample
+
+# Build packages locally
+npm run build:package
+```
+
+#### Running the sample development server
+
+1. In a command prompt, run `npm start`.
+1. Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
+1. Open [http://localhost:3000/profile](http://localhost:3000/profile) to see an example of a protected route. If you are not yet signed in, signin will be invoked automatically.
+
+The page will reload if you make edits.
+You will also see any lint errors in the console.
+
+- In the web page, click on the "Login" button and select either `Sign in using Popup` or `Sign in using Redirect` to begin the auth flow.
+
+#### Running the sample production server
+
+1. In a command prompt, run `npm run build`.
+1. Next run `npx vite preview --port 3000 --strictPort`.
+1. Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
+1. Open [http://localhost:3000/profile](http://localhost:3000/profile) to see an example of a protected route. If you are not yet signed in, signin will be invoked automatically.
+
+#### Learn more about the 3rd-party libraries used to create this sample
+
+- [React documentation](https://reactjs.org/).
+- [Vite documentation](https://vite.dev/guide/)
+- [React Router documentation](https://reactrouter.com/web/guides/quick-start)
+- [Material-UI documentation](https://material-ui.com/getting-started/installation/)
diff --git a/samples/msal-react-samples/react17-sample/index.html b/samples/msal-react-samples/react17-sample/index.html
new file mode 100644
index 0000000000..884d5dc1bd
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+ MSAL-React Sample
+
+
+
+
+
+
+
diff --git a/samples/msal-react-samples/react17-sample/jest.config.cjs b/samples/msal-react-samples/react17-sample/jest.config.cjs
new file mode 100644
index 0000000000..51aa2c62ba
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/jest.config.cjs
@@ -0,0 +1,8 @@
+module.exports = {
+ displayName: "React 17 Compat",
+ globals: {
+ __PORT__: 3000,
+ __STARTCMD__: "env-cmd -f .env.e2e npm start",
+ },
+ preset: "../../e2eTestUtils/jest-puppeteer-utils/jest-preset.js",
+};
diff --git a/samples/msal-react-samples/react17-sample/package.json b/samples/msal-react-samples/react17-sample/package.json
new file mode 100644
index 0000000000..66af55284b
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "react17-sample",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "dependencies": {
+ "@azure/msal-browser": "^5.0.0",
+ "@azure/msal-react": "^5.0.0",
+ "@emotion/react": "^11.10.5",
+ "@emotion/styled": "^11.10.5",
+ "@mui/icons-material": "^5.10.16",
+ "@mui/material": "^5.10.17",
+ "react": "^17.0.0",
+ "react-dom": "^17.0.0",
+ "react-router-dom": "^6.7.0"
+ },
+ "scripts": {
+ "start": "vite",
+ "test:e2e": "jest",
+ "build": "vite build",
+ "build:package": "cd ../../../ && npm run build:all --workspace=lib/msal-react"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "devDependencies": {
+ "@types/jest": "^29.5.0",
+ "@vitejs/plugin-react": "^4.3.3",
+ "e2e-test-utils": "file:../../e2eTestUtils",
+ "env-cmd": "^10.1.0",
+ "jest": "^29.5.0",
+ "jest-junit": "^16.0.0",
+ "ts-jest": "^29.1.0",
+ "vite": "^5.4.21"
+ },
+ "jest-junit": {
+ "suiteNameTemplate": "React 17 Compat Tests",
+ "outputDirectory": ".",
+ "outputName": "test-results.xml"
+ }
+}
\ No newline at end of file
diff --git a/samples/msal-react-samples/react17-sample/public/favicon.ico b/samples/msal-react-samples/react17-sample/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/samples/msal-react-samples/react17-sample/public/favicon.ico differ
diff --git a/samples/msal-react-samples/react17-sample/src/App.jsx b/samples/msal-react-samples/react17-sample/src/App.jsx
new file mode 100644
index 0000000000..6756321e28
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/src/App.jsx
@@ -0,0 +1,62 @@
+import { Routes, Route, useNavigate, useLocation } from "react-router-dom";
+// Material-UI imports
+import Grid from "@mui/material/Grid";
+
+// MSAL imports
+import { MsalProvider } from "@azure/msal-react";
+import { CustomNavigationClient } from "./utils/NavigationClient";
+
+// Sample app imports
+import { PageLayout } from "./ui-components/PageLayout";
+import { Home } from "./pages/Home";
+import { Profile } from "./pages/Profile";
+import { Logout } from "./pages/Logout";
+import { Redirect } from "./pages/Redirect";
+
+// Class-based equivalents of "Profile" component
+import { ProfileWithMsal } from "./pages/ProfileWithMsal";
+import { ProfileRawContext } from "./pages/ProfileRawContext";
+import { ProfileUseMsalAuthenticationHook } from "./pages/ProfileUseMsalAuthenticationHook";
+
+function App({ pca }) {
+ // The next 3 lines are optional. This is how you configure MSAL to take advantage of the router's navigate functions when MSAL redirects between pages in your app
+ const navigate = useNavigate();
+ const location = useLocation();
+ const navigationClient = new CustomNavigationClient(navigate);
+ pca.setNavigationClient(navigationClient);
+
+ // Don't wrap redirect page in MsalProvider to prevent MSAL from consuming the auth response
+ const isRedirectPage = location.pathname === '/redirect';
+
+ if (isRedirectPage) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+function Pages() {
+ return (
+
+ } />
+ } />
+ } />
+ }
+ />
+ } />
+ } />
+
+ );
+}
+
+export default App;
diff --git a/samples/msal-react-samples/react17-sample/src/authConfig.js b/samples/msal-react-samples/react17-sample/src/authConfig.js
new file mode 100644
index 0000000000..73ea34a7f7
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/src/authConfig.js
@@ -0,0 +1,51 @@
+import { LogLevel, BrowserUtils } from "@azure/msal-browser";
+
+// Config object to be passed to Msal on creation
+export const msalConfig = {
+ auth: {
+ clientId: import.meta.env.VITE_CLIENT_ID,
+ authority: import.meta.env.VITE_AUTHORITY,
+ redirectUri: import.meta.env.VITE_REDIRECT_URI,
+ postLogoutRedirectUri: "/",
+ onRedirectNavigate: () => !BrowserUtils.isInIframe()
+ },
+ cache: {
+ cacheLocation: "localStorage",
+ },
+ system: {
+ allowPlatformBroker: false, // Disables WAM Broker
+ loggerOptions: {
+ loggerCallback: (level, message, containsPii) => {
+ if (containsPii) {
+ return;
+ }
+ switch (level) {
+ case LogLevel.Error:
+ console.error(message);
+ return;
+ case LogLevel.Info:
+ console.info(message);
+ return;
+ case LogLevel.Verbose:
+ console.debug(message);
+ return;
+ case LogLevel.Warning:
+ console.warn(message);
+ return;
+ default:
+ return;
+ }
+ },
+ },
+ },
+};
+
+// Add here scopes for id token to be used at MS Identity Platform endpoints.
+export const loginRequest = {
+ scopes: ["User.Read"]
+};
+
+// Add here the endpoints for MS Graph API services you would like to use.
+export const graphConfig = {
+ graphMeEndpoint: "https://graph.microsoft.com/v1.0/me"
+};
diff --git a/samples/msal-react-samples/react17-sample/src/index.jsx b/samples/msal-react-samples/react17-sample/src/index.jsx
new file mode 100644
index 0000000000..6c560955be
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/src/index.jsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { BrowserRouter as Router } from "react-router-dom";
+import { ThemeProvider } from "@mui/material/styles";
+import { theme } from "./styles/theme";
+import App from './App';
+
+// MSAL imports
+import { PublicClientApplication, EventType } from "@azure/msal-browser";
+import { msalConfig } from "./authConfig";
+
+export const msalInstance = new PublicClientApplication(msalConfig);
+
+msalInstance.initialize().then(() => {
+ // 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]);
+ }
+
+ msalInstance.addEventCallback((event) => {
+ if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) {
+ const account = event.payload;
+ msalInstance.setActiveAccount(account);
+ }
+ });
+
+ // React 17 uses ReactDOM.render instead of createRoot
+ ReactDOM.render(
+
+
+
+
+ ,
+ document.getElementById("root")
+ );
+});
diff --git a/samples/msal-react-samples/react17-sample/src/pages/Home.jsx b/samples/msal-react-samples/react17-sample/src/pages/Home.jsx
new file mode 100644
index 0000000000..288c3eb8bb
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/src/pages/Home.jsx
@@ -0,0 +1,26 @@
+import { AuthenticatedTemplate, UnauthenticatedTemplate } from "@azure/msal-react";
+import Button from "@mui/material/Button";
+import ButtonGroup from "@mui/material/ButtonGroup";
+import Typography from "@mui/material/Typography";
+import { Link as RouterLink } from "react-router-dom";
+
+export function Home() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
Please sign-in to see your profile information.
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/samples/msal-react-samples/react17-sample/src/pages/Logout.jsx b/samples/msal-react-samples/react17-sample/src/pages/Logout.jsx
new file mode 100644
index 0000000000..21d765b948
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/src/pages/Logout.jsx
@@ -0,0 +1,16 @@
+import React, { useEffect } from "react";
+import { useMsal } from "@azure/msal-react";
+
+export function Logout() {
+ const { instance } = useMsal();
+
+ useEffect(() => {
+ instance.logoutRedirect({
+ account: instance.getActiveAccount(),
+ })
+ }, [ instance ]);
+
+ return (
+
Logout
+ )
+}
diff --git a/samples/msal-react-samples/react17-sample/src/pages/Profile.jsx b/samples/msal-react-samples/react17-sample/src/pages/Profile.jsx
new file mode 100644
index 0000000000..c015c779f1
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/src/pages/Profile.jsx
@@ -0,0 +1,79 @@
+import { useEffect, useState, useCallback } from "react";
+
+// Msal imports
+import { MsalAuthenticationTemplate, useMsal } from "@azure/msal-react";
+import { EventType, InteractionType, InteractionRequiredAuthError } from "@azure/msal-browser";
+import { loginRequest } from "../authConfig";
+
+// Sample app imports
+import { ProfileData } from "../ui-components/ProfileData";
+import { Loading } from "../ui-components/Loading";
+import { ErrorComponent } from "../ui-components/ErrorComponent";
+import { callMsGraph } from "../utils/MsGraphApiCall";
+
+// Material-ui imports
+import Paper from "@mui/material/Paper";
+
+const ProfileContent = () => {
+ const { instance } = useMsal();
+ const [graphData, setGraphData] = useState(null);
+
+ const fetchProfile = useCallback(() => {
+ if (!instance.getActiveAccount()) {
+ return;
+ }
+ callMsGraph().then(response => setGraphData(response)).catch((e) => {
+ if (e instanceof InteractionRequiredAuthError) {
+ instance.acquireTokenRedirect({
+ ...loginRequest,
+ account: instance.getActiveAccount()
+ });
+ }
+ });
+ }, [instance]);
+
+ useEffect(() => {
+ // Attempt to fetch profile data immediately
+ fetchProfile();
+
+ // Subscribe to active account changes so the Graph call is retried
+ // once setActiveAccount has been called. In React 16/17 the render
+ // triggered by ACQUIRE_TOKEN_SUCCESS fires before the LOGIN_SUCCESS
+ // handler sets the active account, so getActiveAccount() returns null
+ // on the first attempt.
+ const callbackId = instance.addEventCallback((event) => {
+ if (event.eventType === EventType.ACTIVE_ACCOUNT_CHANGED) {
+ fetchProfile();
+ }
+ });
+
+ return () => {
+ if (callbackId) {
+ instance.removeEventCallback(callbackId);
+ }
+ };
+ }, [instance, fetchProfile]);
+
+ return (
+
+ { graphData ? : null }
+
+ );
+};
+
+export function Profile() {
+ const authRequest = {
+ ...loginRequest
+ };
+
+ return (
+
+
+
+ )
+};
\ No newline at end of file
diff --git a/samples/msal-react-samples/react17-sample/src/pages/ProfileRawContext.jsx b/samples/msal-react-samples/react17-sample/src/pages/ProfileRawContext.jsx
new file mode 100644
index 0000000000..38ed1622d5
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/src/pages/ProfileRawContext.jsx
@@ -0,0 +1,112 @@
+import { Component } from "react";
+
+// Msal imports
+import { MsalAuthenticationTemplate, MsalContext } from "@azure/msal-react";
+import { InteractionType, EventType, InteractionRequiredAuthError } from "@azure/msal-browser";
+import { loginRequest } from "../authConfig";
+
+// Sample app imports
+import { ProfileData } from "../ui-components/ProfileData";
+import { Loading } from "../ui-components/Loading";
+import { ErrorComponent } from "../ui-components/ErrorComponent";
+import { callMsGraph } from "../utils/MsGraphApiCall";
+
+// Material-ui imports
+import Paper from "@mui/material/Paper";
+
+
+/**
+ * This class is using the raw context directly. The available
+ * objects and methods are the same as in "withMsal" HOC usage.
+ */
+class ProfileContent extends Component {
+
+ static contextType = MsalContext;
+
+ constructor(props) {
+ super(props)
+
+ this.state = {
+ graphData: null,
+ }
+
+ this.callbackId = null;
+ }
+
+ fetchGraphData() {
+ if (this.state.graphData) {
+ return;
+ }
+
+ const instance = this.context.instance;
+ if (!instance.getActiveAccount()) {
+ return;
+ }
+
+ callMsGraph().then(response => this.setState({graphData: response})).catch((e) => {
+ if (e instanceof InteractionRequiredAuthError) {
+ instance.acquireTokenRedirect({
+ ...loginRequest,
+ account: instance.getActiveAccount()
+ });
+ }
+ });
+ }
+
+ componentDidMount() {
+ // Attempt to fetch profile data immediately
+ this.fetchGraphData();
+
+ // Subscribe to active account changes so the Graph call is retried
+ // once setActiveAccount has been called. In React 16/17 the render
+ // triggered by ACQUIRE_TOKEN_SUCCESS fires before the LOGIN_SUCCESS
+ // handler sets the active account, so getActiveAccount() returns null
+ // on the first attempt.
+ this.callbackId = this.context.instance.addEventCallback((event) => {
+ if (event.eventType === EventType.ACTIVE_ACCOUNT_CHANGED) {
+ this.fetchGraphData();
+ }
+ });
+ }
+
+ componentWillUnmount() {
+ if (this.callbackId) {
+ this.context.instance.removeEventCallback(this.callbackId);
+ }
+ }
+
+ render() {
+ return (
+
+ { this.state.graphData ? : null }
+
+ );
+ }
+}
+
+/**
+ * This class is using "withMsal" HOC. It passes down the msalContext
+ * as a prop to its children.
+ */
+class Profile extends Component {
+
+ render() {
+
+ const authRequest = {
+ ...loginRequest
+ };
+
+ return (
+
+
+
+ );
+ }
+}
+
+export const ProfileRawContext = Profile
diff --git a/samples/msal-react-samples/react17-sample/src/pages/ProfileUseMsalAuthenticationHook.jsx b/samples/msal-react-samples/react17-sample/src/pages/ProfileUseMsalAuthenticationHook.jsx
new file mode 100644
index 0000000000..43bdf3fe11
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/src/pages/ProfileUseMsalAuthenticationHook.jsx
@@ -0,0 +1,51 @@
+import { useEffect, useState } from "react";
+
+// Msal imports
+import { useMsalAuthentication } from "@azure/msal-react";
+import { InteractionType } from "@azure/msal-browser";
+import { loginRequest } from "../authConfig";
+
+// Sample app imports
+import { ProfileData } from "../ui-components/ProfileData";
+import { ErrorComponent } from "../ui-components/ErrorComponent";
+import { callMsGraph } from "../utils/MsGraphApiCall";
+
+// Material-ui imports
+import Paper from "@mui/material/Paper";
+
+const ProfileContent = () => {
+ const [graphData, setGraphData] = useState(null);
+ const { result, error } = useMsalAuthentication(InteractionType.Popup, {
+ ...loginRequest,
+ });
+
+ useEffect(() => {
+ if (!!graphData) {
+ // We already have the data, no need to call the API
+ return;
+ }
+
+ if (!!error) {
+ // Error occurred attempting to acquire a token, either handle the error or do nothing
+ return;
+ }
+
+ if (result) {
+ callMsGraph().then(response => setGraphData(response));
+ }
+ }, [error, result, graphData]);
+
+ if (error) {
+ return ;
+ }
+
+ return (
+
+ { graphData ? : null }
+
+ );
+};
+
+export function ProfileUseMsalAuthenticationHook() {
+ return
+};
diff --git a/samples/msal-react-samples/react17-sample/src/pages/ProfileWithMsal.jsx b/samples/msal-react-samples/react17-sample/src/pages/ProfileWithMsal.jsx
new file mode 100644
index 0000000000..1a2b0b0335
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/src/pages/ProfileWithMsal.jsx
@@ -0,0 +1,87 @@
+import { Component } from "react";
+
+// Msal imports
+import { MsalAuthenticationTemplate, withMsal } from "@azure/msal-react";
+import { InteractionType, InteractionStatus, InteractionRequiredAuthError } from "@azure/msal-browser";
+import { loginRequest } from "../authConfig";
+
+// Sample app imports
+import { ProfileData } from "../ui-components/ProfileData";
+import { Loading } from "../ui-components/Loading";
+import { ErrorComponent } from "../ui-components/ErrorComponent";
+import { callMsGraph } from "../utils/MsGraphApiCall";
+
+// Material-ui imports
+import Paper from "@mui/material/Paper";
+
+/**
+ * This class is a child component of "Profile". MsalContext is passed
+ * down from the parent and available as a prop here.
+ */
+class ProfileContent extends Component {
+
+ constructor(props) {
+ super(props)
+
+ this.state = {
+ graphData: null,
+ }
+ }
+
+ setGraphData() {
+ if (!this.state.graphData && this.props.msalContext.inProgress === InteractionStatus.None) {
+ callMsGraph().then(response => this.setState({graphData: response})).catch((e) => {
+ if (e instanceof InteractionRequiredAuthError) {
+ this.props.msalContext.instance.acquireTokenRedirect({
+ ...loginRequest,
+ account: this.props.msalContext.instance.getActiveAccount()
+ });
+ }
+ });
+ }
+ }
+
+ componentDidMount() {
+ this.setGraphData();
+ }
+
+ componentDidUpdate() {
+ this.setGraphData();
+ }
+
+ render() {
+ return (
+
+ { this.state.graphData ? : null }
+
+ );
+ }
+}
+
+/**
+ * This class is using "withMsal" HOC and has access to authentication
+ * state. It passes down the msalContext as a prop to its children.
+ */
+class Profile extends Component {
+
+ render() {
+
+ const authRequest = {
+ ...loginRequest
+ };
+
+ return (
+
+
+
+ );
+ }
+}
+
+// Wrap your class component to access authentication state as props
+export const ProfileWithMsal = withMsal(Profile);
\ No newline at end of file
diff --git a/samples/msal-react-samples/react17-sample/src/pages/Redirect.jsx b/samples/msal-react-samples/react17-sample/src/pages/Redirect.jsx
new file mode 100644
index 0000000000..dd4529aaf3
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/src/pages/Redirect.jsx
@@ -0,0 +1,20 @@
+// This page serves the redirect bridge for MSAL authentication
+// It's excluded from MsalProvider wrapper in App.js to prevent MSAL from processing the hash
+import { useEffect } from "react";
+import { broadcastResponseToMainFrame } from "@azure/msal-browser/redirect-bridge";
+
+export function Redirect() {
+ useEffect(() => {
+ // Call broadcastResponseToMainFrame when component mounts
+ broadcastResponseToMainFrame().catch((error) => {
+ console.error("Error broadcasting response to main frame:", error);
+ });
+ }, []);
+
+ return (
+
+
Processing authentication...
+
+ );
+}
+
diff --git a/samples/msal-react-samples/react17-sample/src/styles/theme.js b/samples/msal-react-samples/react17-sample/src/styles/theme.js
new file mode 100644
index 0000000000..442acc8121
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/src/styles/theme.js
@@ -0,0 +1,20 @@
+import { unstable_createMuiStrictModeTheme as createMuiTheme } from '@mui/material/styles';
+import { red } from '@mui/material/colors';
+
+// Create a theme instance.
+export const theme = createMuiTheme({
+ palette: {
+ primary: {
+ main: '#556cd6',
+ },
+ secondary: {
+ main: '#19857b',
+ },
+ error: {
+ main: red.A400,
+ },
+ background: {
+ default: '#fff',
+ },
+ },
+});
diff --git a/samples/msal-react-samples/react17-sample/src/ui-components/AccountPicker.jsx b/samples/msal-react-samples/react17-sample/src/ui-components/AccountPicker.jsx
new file mode 100644
index 0000000000..6b1f855fe3
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/src/ui-components/AccountPicker.jsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import { useMsal } from "@azure/msal-react";
+import Avatar from '@mui/material/Avatar';
+import List from '@mui/material/List';
+import ListItem from '@mui/material/ListItem';
+import ListItemAvatar from '@mui/material/ListItemAvatar';
+import ListItemText from '@mui/material/ListItemText';
+import DialogTitle from '@mui/material/DialogTitle';
+import Dialog from '@mui/material/Dialog';
+import PersonIcon from '@mui/icons-material/Person';
+import AddIcon from '@mui/icons-material/Add';
+import { loginRequest } from "../authConfig";
+
+export const AccountPicker = (props) => {
+ const { instance, accounts } = useMsal();
+ const { onClose, open } = props;
+
+ const handleListItemClick = (account) => {
+ instance.setActiveAccount(account);
+ if (!account) {
+ instance.loginRedirect({
+ ...loginRequest,
+ prompt: "login"
+ })
+ } else {
+ // To ensure account related page attributes update after the account is changed
+ window.location.reload();
+ }
+
+ onClose(account);
+ };
+
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/samples/msal-react-samples/react17-sample/src/ui-components/ErrorComponent.jsx b/samples/msal-react-samples/react17-sample/src/ui-components/ErrorComponent.jsx
new file mode 100644
index 0000000000..de8ddd2607
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/src/ui-components/ErrorComponent.jsx
@@ -0,0 +1,5 @@
+import { Typography } from "@mui/material";
+
+export const ErrorComponent = ({error}) => {
+ return An Error Occurred: {error.errorCode};
+}
\ No newline at end of file
diff --git a/samples/msal-react-samples/react17-sample/src/ui-components/Loading.jsx b/samples/msal-react-samples/react17-sample/src/ui-components/Loading.jsx
new file mode 100644
index 0000000000..c2ae494c80
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/src/ui-components/Loading.jsx
@@ -0,0 +1,5 @@
+import { Typography } from "@mui/material";
+
+export const Loading = () => {
+ return Authentication in progress...
+}
\ No newline at end of file
diff --git a/samples/msal-react-samples/react17-sample/src/ui-components/NavBar.jsx b/samples/msal-react-samples/react17-sample/src/ui-components/NavBar.jsx
new file mode 100644
index 0000000000..bea9940fbc
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/src/ui-components/NavBar.jsx
@@ -0,0 +1,25 @@
+import AppBar from "@mui/material/AppBar";
+import Toolbar from "@mui/material/Toolbar";
+import Link from "@mui/material/Link";
+import Typography from "@mui/material/Typography";
+import WelcomeName from "./WelcomeName";
+import SignInSignOutButton from "./SignInSignOutButton";
+import { Link as RouterLink } from "react-router-dom";
+
+const NavBar = () => {
+ return (
+
+
+
+
+ MS Identity Platform
+
+
+
+
+
+
+ );
+};
+
+export default NavBar;
\ No newline at end of file
diff --git a/samples/msal-react-samples/react17-sample/src/ui-components/PageLayout.jsx b/samples/msal-react-samples/react17-sample/src/ui-components/PageLayout.jsx
new file mode 100644
index 0000000000..f4e5672879
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/src/ui-components/PageLayout.jsx
@@ -0,0 +1,16 @@
+import Typography from "@mui/material/Typography";
+import NavBar from "./NavBar";
+
+export const PageLayout = (props) => {
+ return (
+ <>
+
+
+
Welcome to the Microsoft Authentication Library For React Quickstart
+
+
+
+ {props.children}
+ >
+ );
+};
\ No newline at end of file
diff --git a/samples/msal-react-samples/react17-sample/src/ui-components/ProfileData.jsx b/samples/msal-react-samples/react17-sample/src/ui-components/ProfileData.jsx
new file mode 100644
index 0000000000..f7aa6223c7
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/src/ui-components/ProfileData.jsx
@@ -0,0 +1,78 @@
+import React from "react";
+import List from "@mui/material/List";
+import ListItem from "@mui/material/ListItem";
+import ListItemText from "@mui/material/ListItemText";
+import ListItemAvatar from "@mui/material/ListItemAvatar";
+import Avatar from "@mui/material/Avatar";
+import PersonIcon from '@mui/icons-material/Person';
+import WorkIcon from "@mui/icons-material/Work";
+import MailIcon from '@mui/icons-material/Mail';
+import PhoneIcon from '@mui/icons-material/Phone';
+import LocationOnIcon from '@mui/icons-material/LocationOn';
+
+export const ProfileData = ({graphData}) => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+const NameListItem = ({name}) => (
+
+
+
+
+
+
+
+
+);
+
+const JobTitleListItem = ({jobTitle}) => (
+
+
+
+
+
+
+
+
+);
+
+const MailListItem = ({mail}) => (
+
+
+
+
+
+
+
+
+);
+
+const PhoneListItem = ({phone}) => (
+
+
+
+
+
+
+
+
+);
+
+const LocationListItem = ({location}) => (
+
+
+
+
+
+
+
+
+);
diff --git a/samples/msal-react-samples/react17-sample/src/ui-components/SignInButton.jsx b/samples/msal-react-samples/react17-sample/src/ui-components/SignInButton.jsx
new file mode 100644
index 0000000000..9ffc1751ef
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/src/ui-components/SignInButton.jsx
@@ -0,0 +1,161 @@
+import { useState, useRef, useEffect } from "react";
+import { useMsal } from "@azure/msal-react";
+import Button from "@mui/material/Button";
+import MenuItem from '@mui/material/MenuItem';
+import Menu from '@mui/material/Menu';
+import Alert from '@mui/material/Alert';
+import AlertTitle from '@mui/material/AlertTitle';
+import Dialog from '@mui/material/Dialog';
+import DialogActions from '@mui/material/DialogActions';
+import DialogContent from '@mui/material/DialogContent';
+import DialogContentText from '@mui/material/DialogContentText';
+import DialogTitle from '@mui/material/DialogTitle';
+import { loginRequest } from "../authConfig";
+
+export const SignInButton = () => {
+ const { instance } = useMsal();
+
+ const [anchorEl, setAnchorEl] = useState(null);
+ const [showRetryDialog, setShowRetryDialog] = useState(false);
+ const [retryRequested, setRetryRequested] = useState(false);
+ const [showPopupWarning, setShowPopupWarning] = useState(false);
+ const open = Boolean(anchorEl);
+
+ // Track mounted state to avoid setting state after unmount (React 17 does not batch async state updates)
+ const isMountedRef = useRef(true);
+ useEffect(() => {
+ return () => {
+ isMountedRef.current = false;
+ };
+ }, []);
+
+ const handleLogin = async (loginType) => {
+ setAnchorEl(null);
+
+ if (loginType === "popup") {
+ // Show warning when popup is about to open
+ setShowPopupWarning(true);
+
+ try {
+ await instance.loginPopup({
+ ...loginRequest,
+ // Only override if user explicitly clicked retry
+ overrideInteractionInProgress: retryRequested
+ });
+
+ // Hide warning on success — guard against unmounted component
+ if (isMountedRef.current) {
+ setShowPopupWarning(false);
+ setRetryRequested(false);
+ }
+ } catch (error) {
+ // Hide warning on error — guard against unmounted component
+ if (isMountedRef.current) {
+ setShowPopupWarning(false);
+
+ if (error.errorCode === 'interaction_in_progress') {
+ // Show retry dialog - let user decide whether to retry
+ setShowRetryDialog(true);
+ } else {
+ // Reset retry flag for other errors
+ setRetryRequested(false);
+ console.error(error);
+ }
+ }
+ }
+ } else if (loginType === "redirect") {
+ instance.loginRedirect(loginRequest);
+ }
+ }
+
+ const handleRetry = () => {
+ setShowRetryDialog(false);
+ setRetryRequested(true); // User explicitly requested retry
+ handleLogin("popup");
+ }
+
+ const handleCancelRetry = () => {
+ setShowRetryDialog(false);
+ setRetryRequested(false);
+ }
+
+ return (
+
+
+
+
+ {/* Warning message during popup authentication */}
+ {showPopupWarning && (
+
+ Authentication in Progress
+ Please complete authentication in the popup window. Do not close the popup until authentication is complete.
+
+ )}
+
+ {/* Retry dialog for interaction_in_progress errors */}
+
+
+ )
+};
diff --git a/samples/msal-react-samples/react17-sample/src/ui-components/SignInSignOutButton.jsx b/samples/msal-react-samples/react17-sample/src/ui-components/SignInSignOutButton.jsx
new file mode 100644
index 0000000000..61633e1459
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/src/ui-components/SignInSignOutButton.jsx
@@ -0,0 +1,20 @@
+import { useIsAuthenticated, useMsal } from "@azure/msal-react";
+import { SignInButton } from "./SignInButton";
+import { SignOutButton } from "./SignOutButton";
+import { InteractionStatus } from "@azure/msal-browser";
+
+const SignInSignOutButton = () => {
+ const { inProgress } = useMsal();
+ const isAuthenticated = useIsAuthenticated();
+
+ if (isAuthenticated) {
+ return ;
+ } else if (inProgress !== InteractionStatus.Startup && inProgress !== InteractionStatus.HandleRedirect) {
+ // inProgress check prevents sign-in button from being displayed briefly after returning from a redirect sign-in. Processing the server response takes a render cycle or two
+ return ;
+ } else {
+ return null;
+ }
+}
+
+export default SignInSignOutButton;
\ No newline at end of file
diff --git a/samples/msal-react-samples/react17-sample/src/ui-components/SignOutButton.jsx b/samples/msal-react-samples/react17-sample/src/ui-components/SignOutButton.jsx
new file mode 100644
index 0000000000..2a9fbd451e
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/src/ui-components/SignOutButton.jsx
@@ -0,0 +1,65 @@
+import { useState } from "react";
+import { useMsal } from "@azure/msal-react";
+import IconButton from '@mui/material/IconButton';
+import AccountCircle from "@mui/icons-material/AccountCircle";
+import MenuItem from '@mui/material/MenuItem';
+import Menu from '@mui/material/Menu';
+import { AccountPicker } from "./AccountPicker";
+
+export const SignOutButton = () => {
+ const { instance } = useMsal();
+ const [accountSelectorOpen, setOpen] = useState(false);
+
+ const [anchorEl, setAnchorEl] = useState(null);
+ const open = Boolean(anchorEl);
+
+ const handleLogout = (logoutType) => {
+ setAnchorEl(null);
+
+ if (logoutType === "popup") {
+ instance.logoutPopup();
+ } else if (logoutType === "redirect") {
+ instance.logoutRedirect();
+ }
+ }
+
+ const handleAccountSelection = () => {
+ setAnchorEl(null);
+ setOpen(true);
+ }
+
+ const handleClose = () => {
+ setOpen(false);
+ };
+
+ return (
+
+ )
+};
\ No newline at end of file
diff --git a/samples/msal-react-samples/react17-sample/src/ui-components/WelcomeName.jsx b/samples/msal-react-samples/react17-sample/src/ui-components/WelcomeName.jsx
new file mode 100644
index 0000000000..ac4ad1754c
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/src/ui-components/WelcomeName.jsx
@@ -0,0 +1,47 @@
+import { useEffect, useState, useCallback } from "react";
+import { useMsal } from "@azure/msal-react";
+import { EventType } from "@azure/msal-browser";
+import Typography from "@mui/material/Typography";
+
+const WelcomeName = () => {
+ const { instance } = useMsal();
+ const [name, setName] = useState(null);
+
+ const updateName = useCallback(() => {
+ const activeAccount = instance.getActiveAccount();
+ if (activeAccount) {
+ setName(activeAccount.name.split(' ')[0]);
+ } else {
+ setName(null);
+ }
+ }, [instance]);
+
+ useEffect(() => {
+ // Set the name from the current active account on mount
+ updateName();
+
+ // Subscribe to active account changes so the component updates when
+ // setActiveAccount is called. This avoids the React 16/17 batching issue where the
+ // render triggered by ACQUIRE_TOKEN_SUCCESS runs before setActiveAccount
+ // has been called, causing getActiveAccount() to return null.
+ const callbackId = instance.addEventCallback((event) => {
+ if (event.eventType === EventType.ACTIVE_ACCOUNT_CHANGED) {
+ updateName();
+ }
+ });
+
+ return () => {
+ if (callbackId) {
+ instance.removeEventCallback(callbackId);
+ }
+ };
+ }, [instance, updateName]);
+
+ if (name) {
+ return Welcome, {name};
+ } else {
+ return null;
+ }
+};
+
+export default WelcomeName;
\ No newline at end of file
diff --git a/samples/msal-react-samples/react17-sample/src/utils/MsGraphApiCall.js b/samples/msal-react-samples/react17-sample/src/utils/MsGraphApiCall.js
new file mode 100644
index 0000000000..360493a86b
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/src/utils/MsGraphApiCall.js
@@ -0,0 +1,33 @@
+import { loginRequest, graphConfig } from "../authConfig";
+import { msalInstance } from "../index";
+
+export async function callMsGraph(accessToken) {
+ if (!accessToken) {
+ const account = msalInstance.getActiveAccount();
+ if (!account) {
+ throw Error("No active account! Verify a user has been signed in and setActiveAccount has been called.");
+ }
+
+ const response = await msalInstance.acquireTokenSilent({
+ ...loginRequest,
+ account: account
+ });
+ accessToken = response.accessToken;
+ }
+
+ const headers = new Headers();
+ const bearer = `Bearer ${accessToken}`;
+
+ headers.append("Authorization", bearer);
+
+ const options = {
+ method: "GET",
+ headers: headers
+ };
+
+ const response = await fetch(graphConfig.graphMeEndpoint, options);
+ if (!response.ok) {
+ throw new Error(`MS Graph request failed: ${response.status} ${response.statusText}`);
+ }
+ return response.json();
+}
diff --git a/samples/msal-react-samples/react17-sample/src/utils/NavigationClient.js b/samples/msal-react-samples/react17-sample/src/utils/NavigationClient.js
new file mode 100644
index 0000000000..70eab8c6d7
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/src/utils/NavigationClient.js
@@ -0,0 +1,28 @@
+import { NavigationClient } from "@azure/msal-browser";
+
+/**
+ * This is an example for overriding the default function MSAL uses to navigate to other urls in your webpage
+ */
+export class CustomNavigationClient extends NavigationClient {
+ constructor(navigate) {
+ super();
+ this.navigate = navigate;
+ }
+
+ /**
+ * Navigates to other pages within the same web application
+ * You can use the useNavigate hook provided by react-router-dom to take advantage of client-side routing
+ * @param url
+ * @param options
+ */
+ async navigateInternal(url, options) {
+ const relativePath = url.replace(window.location.origin, "");
+ if (options.noHistory) {
+ this.navigate(relativePath, { replace: true });
+ } else {
+ this.navigate(relativePath);
+ }
+
+ return false;
+ }
+}
diff --git a/samples/msal-react-samples/react17-sample/test/home.spec.ts b/samples/msal-react-samples/react17-sample/test/home.spec.ts
new file mode 100644
index 0000000000..eb2c005709
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/test/home.spec.ts
@@ -0,0 +1,148 @@
+import * as puppeteer from "puppeteer";
+import {
+ Screenshot,
+ setupCredentials,
+ enterCredentials,
+ RETRY_TIMES,
+ LabClient,
+ LabApiQueryParams,
+ AzureEnvironments,
+ AppTypes,
+ BrowserCacheUtils,
+} from "e2e-test-utils";
+
+const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/home-tests`;
+
+describe("/ (Home Page)", () => {
+ jest.retryTimes(RETRY_TIMES);
+ let browser: puppeteer.Browser;
+ let context: puppeteer.BrowserContext;
+ let page: puppeteer.Page;
+ let port: number;
+ let username: string;
+ let accountPwd: string;
+ let BrowserCache: BrowserCacheUtils;
+
+ beforeAll(async () => {
+ // @ts-ignore
+ browser = await global.__BROWSER__;
+ // @ts-ignore
+ port = global.__PORT__;
+
+ const labApiParams: LabApiQueryParams = {
+ azureEnvironment: AzureEnvironments.CLOUD,
+ appType: AppTypes.CLOUD,
+ };
+
+ const labClient = new LabClient();
+ const envResponse = await labClient.getVarsByCloudEnvironment(
+ labApiParams
+ );
+
+ [username, accountPwd] = await setupCredentials(
+ envResponse[0],
+ labClient
+ );
+ });
+
+ beforeEach(async () => {
+ context = await browser.createBrowserContext();
+ page = await context.newPage();
+ page.setDefaultTimeout(5000);
+ BrowserCache = new BrowserCacheUtils(page, "localStorage");
+ await page.goto(`http://localhost:${port}`);
+ });
+
+ afterEach(async () => {
+ await page.close();
+ await context.close();
+ });
+
+ it("AuthenticatedTemplate - children are rendered after logging in with loginRedirect", async () => {
+ const testName = "redirectBaseCase";
+ const screenshot = new Screenshot(
+ `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}`
+ );
+ await screenshot.takeScreenshot(page, "Page loaded");
+
+ // Initiate Login
+ const signInButton = await page.waitForSelector(
+ "xpath=//button[contains(., 'Login')]"
+ );
+ await signInButton.click();
+ await screenshot.takeScreenshot(page, "Login button clicked");
+ const loginRedirectButton = await page.waitForSelector(
+ "xpath=//li[contains(., 'Sign in using Redirect')]"
+ );
+ await loginRedirectButton.click();
+
+ await enterCredentials(page, screenshot, username, accountPwd);
+ await screenshot.takeScreenshot(page, "Returned to app");
+
+ // Verify UI now displays logged in content
+ await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]");
+ const profileButton = await page.waitForSelector(
+ "xpath=//header//button"
+ );
+ await profileButton.click();
+ const logoutButtons = await page.$$(
+ "xpath/.//li[contains(., 'Logout using')]"
+ );
+ expect(logoutButtons.length).toBe(2);
+ await screenshot.takeScreenshot(page, "App signed in");
+
+ // Verify tokens are in cache
+ await BrowserCache.verifyTokenStore({
+ scopes: ['User.Read'],
+ });
+ });
+
+ it("AuthenticatedTemplate - children are rendered after logging in with loginPopup", async () => {
+ const testName = "popupBaseCase";
+ const screenshot = new Screenshot(
+ `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}`
+ );
+ await screenshot.takeScreenshot(page, "Page loaded");
+
+ // Initiate Login
+ const signInButton = await page.waitForSelector(
+ "xpath=//button[contains(., 'Login')]"
+ );
+ await signInButton.click();
+ await screenshot.takeScreenshot(page, "Login button clicked");
+ const loginPopupButton = await page.waitForSelector(
+ "xpath=//li[contains(., 'Sign in using Popup')]"
+ );
+ const newPopupWindowPromise = new Promise((resolve) =>
+ page.once("popup", resolve)
+ );
+ await loginPopupButton.click();
+ const popupPage = await newPopupWindowPromise;
+ if (!popupPage) {
+ throw new Error('Popup window was not opened');
+ }
+
+ await enterCredentials(popupPage, screenshot, username, accountPwd);
+ await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]", {
+ timeout: 3000,
+ });
+ await screenshot.takeScreenshot(page, "Popup closed");
+
+ // Verify UI now displays logged in content
+ await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]");
+ const profileButton = await page.waitForSelector(
+ "xpath=//header//button"
+ );
+ await profileButton.click();
+ const logoutButtons = await page.$$(
+ "xpath/.//li[contains(., 'Logout using')]"
+ );
+ expect(logoutButtons.length).toBe(2);
+ await screenshot.takeScreenshot(page, "App signed in");
+
+ // Verify tokens are in cache
+ await BrowserCache.verifyTokenStore({
+ scopes: ['User.Read'],
+ });
+ });
+});
diff --git a/samples/msal-react-samples/react17-sample/test/profile.spec.ts b/samples/msal-react-samples/react17-sample/test/profile.spec.ts
new file mode 100644
index 0000000000..e3b383cf66
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/test/profile.spec.ts
@@ -0,0 +1,170 @@
+import * as puppeteer from "puppeteer";
+import {
+ Screenshot,
+ setupCredentials,
+ enterCredentials,
+ RETRY_TIMES,
+ LabClient,
+ LabApiQueryParams,
+ AzureEnvironments,
+ AppTypes,
+ BrowserCacheUtils,
+} from "e2e-test-utils";
+
+const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/profile-tests`;
+
+async function verifyTokenStore(
+ BrowserCache: BrowserCacheUtils,
+ scopes: string[]
+): Promise {
+ await BrowserCache.verifyTokenStore({
+ scopes,
+ });
+ const telemetryCacheEntry = await BrowserCache.getTelemetryCacheEntry(
+ "b5c2e510-4a17-4feb-b219-e55aa5b74144"
+ );
+ expect(telemetryCacheEntry).not.toBeNull();
+ expect(telemetryCacheEntry["cacheHits"]).toBeGreaterThanOrEqual(1);
+}
+
+describe("/profile", () => {
+ jest.retryTimes(RETRY_TIMES);
+ let browser: puppeteer.Browser;
+ let context: puppeteer.BrowserContext;
+ let page: puppeteer.Page;
+ let port: number;
+ let username: string;
+ let accountPwd: string;
+ let BrowserCache: BrowserCacheUtils;
+
+ beforeAll(async () => {
+ // @ts-ignore
+ browser = await global.__BROWSER__;
+ // @ts-ignore
+ port = global.__PORT__;
+
+ const labApiParams: LabApiQueryParams = {
+ azureEnvironment: AzureEnvironments.CLOUD,
+ appType: AppTypes.CLOUD,
+ };
+
+ const labClient = new LabClient();
+ const envResponse = await labClient.getVarsByCloudEnvironment(
+ labApiParams
+ );
+
+ [username, accountPwd] = await setupCredentials(
+ envResponse[0],
+ labClient
+ );
+ });
+
+ beforeEach(async () => {
+ context = await browser.createBrowserContext();
+ page = await context.newPage();
+ page.setDefaultTimeout(5000);
+ BrowserCache = new BrowserCacheUtils(page, "localStorage");
+ await page.goto(`http://localhost:${port}`);
+ });
+
+ afterEach(async () => {
+ await page.close();
+ await context.close();
+ });
+
+ it("MsalAuthenticationTemplate - invokes loginPopup if user is not signed in", async () => {
+ const testName = "MsalAuthenticationTemplateBaseCase";
+ const screenshot = new Screenshot(
+ `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}`
+ );
+ await screenshot.takeScreenshot(page, "Home page loaded");
+
+ // Navigate to /profile and expect popup to be opened without interaction
+ const newPopupWindowPromise = new Promise((resolve) =>
+ page.once("popup", resolve)
+ );
+ await page.goto(`http://localhost:${port}/profile`);
+ await screenshot.takeScreenshot(page, "Profile page loaded");
+ const popupPage = await newPopupWindowPromise;
+ if (!popupPage) {
+ throw new Error('Popup window was not opened');
+ }
+
+ await enterCredentials(popupPage, screenshot, username, accountPwd);
+
+ // Wait for Graph data to display
+ await page.waitForSelector("xpath/.//div/ul/li[contains(., 'Name')]", {
+ timeout: 5000,
+ });
+ await screenshot.takeScreenshot(page, "Graph data acquired");
+
+ // Verify UI now displays logged in content
+ await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]");
+ const profileButton = await page.waitForSelector(
+ "xpath=//header//button"
+ );
+ await profileButton.click();
+ const logoutButtons = await page.$$(
+ "xpath/.//li[contains(., 'Logout using')]"
+ );
+ expect(logoutButtons.length).toBe(2);
+ await screenshot.takeScreenshot(page, "App signed in");
+
+ // Verify tokens are in cache
+ await verifyTokenStore(BrowserCache, ["User.Read"]);
+ });
+
+ it("MsalAuthenticationTemplate - renders children without invoking login if user is already signed in", async () => {
+ const testName = "MsalAuthenticationTemplateSignedInCase";
+ const screenshot = new Screenshot(
+ `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}`
+ );
+ await screenshot.takeScreenshot(page, "Page loaded");
+
+ // Initiate Login
+ const signInButton = await page.waitForSelector(
+ "xpath=//button[contains(., 'Login')]"
+ );
+ await signInButton.click();
+ await screenshot.takeScreenshot(page, "Login button clicked");
+ const loginPopupButton = await page.waitForSelector(
+ "xpath=//li[contains(., 'Sign in using Popup')]"
+ );
+ const newPopupWindowPromise = new Promise((resolve) =>
+ page.once("popup", resolve)
+ );
+ await loginPopupButton.click();
+ const popupPage = await newPopupWindowPromise;
+ if (!popupPage) {
+ throw new Error('Popup window was not opened');
+ }
+
+ await enterCredentials(popupPage, screenshot, username, accountPwd);
+ await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]", {
+ timeout: 3000,
+ });
+ await screenshot.takeScreenshot(page, "Popup closed");
+
+ // Verify UI now displays logged in content
+ await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]");
+ const profileButton = await page.waitForSelector(
+ "xpath=//header//button"
+ );
+ await profileButton.click();
+ const logoutButtons = await page.$$(
+ "xpath/.//li[contains(., 'Logout using')]"
+ );
+ expect(logoutButtons.length).toBe(2);
+ await screenshot.takeScreenshot(page, "App signed in");
+
+ // Go to protected page
+ await page.goto(`http://localhost:${port}/profile`);
+ // Wait for Graph data to display
+ await page.waitForSelector("xpath/.//div/ul/li[contains(., 'Name')]", {
+ timeout: 5000,
+ });
+ await screenshot.takeScreenshot(page, "Graph data acquired");
+ // Verify tokens are in cache
+ await verifyTokenStore(BrowserCache, ["User.Read"]);
+ });
+});
diff --git a/samples/msal-react-samples/react17-sample/test/profileRawContext.spec.ts b/samples/msal-react-samples/react17-sample/test/profileRawContext.spec.ts
new file mode 100644
index 0000000000..ff4df3c2a2
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/test/profileRawContext.spec.ts
@@ -0,0 +1,104 @@
+import * as puppeteer from "puppeteer";
+import {
+ Screenshot,
+ setupCredentials,
+ enterCredentials,
+ RETRY_TIMES,
+ LabClient,
+ LabApiQueryParams,
+ AzureEnvironments,
+ AppTypes,
+ BrowserCacheUtils,
+} from "e2e-test-utils";
+
+const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/profileRawContext-tests`;
+
+async function verifyTokenStore(
+ BrowserCache: BrowserCacheUtils,
+ scopes: string[]
+): Promise {
+ await BrowserCache.verifyTokenStore({
+ scopes,
+ });
+ const telemetryCacheEntry = await BrowserCache.getTelemetryCacheEntry(
+ "b5c2e510-4a17-4feb-b219-e55aa5b74144"
+ );
+ expect(telemetryCacheEntry).not.toBeNull();
+ expect(telemetryCacheEntry["cacheHits"]).toBeGreaterThanOrEqual(1);
+}
+
+describe("/profileRawContext", () => {
+ jest.retryTimes(RETRY_TIMES);
+ let browser: puppeteer.Browser;
+ let context: puppeteer.BrowserContext;
+ let page: puppeteer.Page;
+ let port: number;
+ let username: string;
+ let accountPwd: string;
+ let BrowserCache: BrowserCacheUtils;
+
+ beforeAll(async () => {
+ // @ts-ignore
+ browser = await global.__BROWSER__;
+ // @ts-ignore
+ port = global.__PORT__;
+
+ const labApiParams: LabApiQueryParams = {
+ azureEnvironment: AzureEnvironments.CLOUD,
+ appType: AppTypes.CLOUD,
+ };
+
+ const labClient = new LabClient();
+ const envResponse = await labClient.getVarsByCloudEnvironment(
+ labApiParams
+ );
+
+ [username, accountPwd] = await setupCredentials(
+ envResponse[0],
+ labClient
+ );
+ });
+
+ beforeEach(async () => {
+ context = await browser.createBrowserContext();
+ page = await context.newPage();
+ page.setDefaultTimeout(5000);
+ BrowserCache = new BrowserCacheUtils(page, "localStorage");
+ await page.goto(`http://localhost:${port}`);
+ });
+
+ afterEach(async () => {
+ await page.close();
+ await context.close();
+ });
+
+ it("MsalAuthenticationTemplate - invokes loginPopup if user is not signed in (class component w/ raw context)", async () => {
+ const testName = "MsalAuthenticationTemplatePopupCase";
+ const screenshot = new Screenshot(
+ `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}`
+ );
+ await screenshot.takeScreenshot(page, "Home page loaded");
+
+ // Navigate to /profile and expect popup to be opened without interaction
+ const newPopupWindowPromise = new Promise((resolve) =>
+ page.once("popup", resolve)
+ );
+ await page.goto(`http://localhost:${port}/profileRawContext`);
+ await screenshot.takeScreenshot(page, "Profile page loaded");
+ const popupPage = await newPopupWindowPromise;
+ if (!popupPage) {
+ throw new Error('Popup window was not opened');
+ }
+
+ await enterCredentials(popupPage, screenshot, username, accountPwd);
+
+ // Wait for Graph data to display
+ await page.waitForSelector("xpath/.//div/ul/li[contains(., 'Name')]", {
+ timeout: 5000,
+ });
+ await screenshot.takeScreenshot(page, "Graph data acquired");
+
+ // Verify tokens are in cache
+ await verifyTokenStore(BrowserCache, ["User.Read"]);
+ });
+});
diff --git a/samples/msal-react-samples/react17-sample/test/profileWithMsal.spec.ts b/samples/msal-react-samples/react17-sample/test/profileWithMsal.spec.ts
new file mode 100644
index 0000000000..1106546587
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/test/profileWithMsal.spec.ts
@@ -0,0 +1,97 @@
+import * as puppeteer from "puppeteer";
+import {
+ Screenshot,
+ setupCredentials,
+ enterCredentials,
+ RETRY_TIMES,
+ LabClient,
+ LabApiQueryParams,
+ AzureEnvironments,
+ AppTypes,
+ BrowserCacheUtils,
+} from "e2e-test-utils";
+
+const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/profileWithMsal-tests`;
+
+async function verifyTokenStore(
+ BrowserCache: BrowserCacheUtils,
+ scopes: string[]
+): Promise {
+ await BrowserCache.verifyTokenStore({
+ scopes,
+ });
+ const telemetryCacheEntry = await BrowserCache.getTelemetryCacheEntry(
+ "b5c2e510-4a17-4feb-b219-e55aa5b74144"
+ );
+ expect(telemetryCacheEntry).not.toBeNull();
+ expect(telemetryCacheEntry["cacheHits"]).toBeGreaterThanOrEqual(1);
+}
+
+describe("/profileWithMsal", () => {
+ jest.retryTimes(RETRY_TIMES);
+ let browser: puppeteer.Browser;
+ let context: puppeteer.BrowserContext;
+ let page: puppeteer.Page;
+ let port: number;
+ let username: string;
+ let accountPwd: string;
+ let BrowserCache: BrowserCacheUtils;
+
+ beforeAll(async () => {
+ // @ts-ignore
+ browser = await global.__BROWSER__;
+ // @ts-ignore
+ port = global.__PORT__;
+
+ const labApiParams: LabApiQueryParams = {
+ azureEnvironment: AzureEnvironments.CLOUD,
+ appType: AppTypes.CLOUD,
+ };
+
+ const labClient = new LabClient();
+ const envResponse = await labClient.getVarsByCloudEnvironment(
+ labApiParams
+ );
+
+ [username, accountPwd] = await setupCredentials(
+ envResponse[0],
+ labClient
+ );
+ });
+
+ beforeEach(async () => {
+ context = await browser.createBrowserContext();
+ page = await context.newPage();
+ page.setDefaultTimeout(5000);
+ BrowserCache = new BrowserCacheUtils(page, "localStorage");
+ await page.goto(`http://localhost:${port}`);
+ });
+
+ afterEach(async () => {
+ await page.close();
+ await context.close();
+ });
+
+ it("MsalAuthenticationTemplate - invokes loginRedirect if user is not signed in (class component w/ withMsal HOC)", async () => {
+ const testName = "MsalAuthenticationTemplateRedirectCase";
+ const screenshot = new Screenshot(
+ `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}`
+ );
+ await screenshot.takeScreenshot(page, "Home page loaded");
+
+ // Navigate to /profileWithMsal and expect redirect to be initiated without interaction
+ await page.goto(`http://localhost:${port}/profileWithMsal`);
+ await screenshot.takeScreenshot(page, "Profile page loaded");
+
+ await enterCredentials(page, screenshot, username, accountPwd);
+
+ // Wait for Graph data to display
+ await page.waitForSelector("xpath/.//div/ul/li[contains(., 'Name')]", {
+ timeout: 5000,
+ });
+ await screenshot.takeScreenshot(page, "Graph data acquired");
+
+ // Verify tokens are in cache
+ await verifyTokenStore(BrowserCache, ["User.Read"]);
+ });
+});
diff --git a/samples/msal-react-samples/react17-sample/tsconfig.json b/samples/msal-react-samples/react17-sample/tsconfig.json
new file mode 100644
index 0000000000..a8df22f95c
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../tsconfig.json"
+}
\ No newline at end of file
diff --git a/samples/msal-react-samples/react17-sample/vite.config.js b/samples/msal-react-samples/react17-sample/vite.config.js
new file mode 100644
index 0000000000..9be5c7392c
--- /dev/null
+++ b/samples/msal-react-samples/react17-sample/vite.config.js
@@ -0,0 +1,10 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 3000,
+ strictPort: true,
+ },
+});
diff --git a/samples/msal-react-samples/react18-sample/.env.development b/samples/msal-react-samples/react18-sample/.env.development
new file mode 100644
index 0000000000..b8e1347305
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/.env.development
@@ -0,0 +1,5 @@
+BROWSER=none
+
+VITE_CLIENT_ID=ENTER_CLIENT_ID_HERE
+VITE_AUTHORITY=https://login.microsoftonline.com/ENTER_TENANT_ID_HERE
+VITE_REDIRECT_URI=/redirect
diff --git a/samples/msal-react-samples/react18-sample/.env.e2e b/samples/msal-react-samples/react18-sample/.env.e2e
new file mode 100644
index 0000000000..7b1d8d02ec
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/.env.e2e
@@ -0,0 +1,5 @@
+BROWSER=none
+
+VITE_CLIENT_ID=b5c2e510-4a17-4feb-b219-e55aa5b74144
+VITE_AUTHORITY=https://login.microsoftonline.com/common
+VITE_REDIRECT_URI=/redirect
diff --git a/samples/msal-react-samples/react18-sample/.gitignore b/samples/msal-react-samples/react18-sample/.gitignore
new file mode 100644
index 0000000000..227a007b62
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/.gitignore
@@ -0,0 +1,20 @@
+# dependencies
+/node_modules
+
+# testing
+/coverage
+
+# production
+/build
+/dist
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
diff --git a/samples/msal-react-samples/react18-sample/.npmrc b/samples/msal-react-samples/react18-sample/.npmrc
new file mode 100644
index 0000000000..43c97e719a
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/.npmrc
@@ -0,0 +1 @@
+package-lock=false
diff --git a/samples/msal-react-samples/react18-sample/README.md b/samples/msal-react-samples/react18-sample/README.md
new file mode 100644
index 0000000000..22a2850b06
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/README.md
@@ -0,0 +1,87 @@
+# MSAL.js for React Sample - React 18 Compatibility
+
+## About this sample
+
+This sample is derived from the [react-router-sample](../react-router-sample) and demonstrates MSAL React running with **React 18**. It uses `ReactDOM.createRoot` (React 18+) for rendering and MUI v5 (`@mui/material`) for UI components.
+
+## Notable files and what they demonstrate
+
+1. `./src/App.jsx` - Shows implementation of `MsalProvider`, all children will have access to `@azure/msal-react` context, hooks and components.
+1. `./src/index.jsx` - Shows initialization of the `PublicClientApplication` that is passed to `App.jsx`
+1. `./src/pages/Home.jsx` - Homepage, shows how to conditionally render content using `AuthenticatedTemplate` and `UnauthenticatedTemplate` depending on whether or not a user is signed in.
+1. `./src/pages/Profile.jsx` - Example of a protected route using `MsalAuthenticationTemplate`. If a user is not yet signed in, signin will be invoked automatically. If a user is signed in it will acquire an access token and make a call to MS Graph to fetch user profile data.
+1. `./src/authConfig.js` - Configuration options for `PublicClientApplication` and token requests.
+1. `./src/ui-components/SignInSignOutButton.jsx` - Example of how to conditionally render a Sign In or Sign Out button using the `useIsAuthenticated` hook.
+1. `./src/ui-components/SignInButton.jsx` - Example of how to get the `PublicClientApplication` instance using the `useMsal` hook and invoking a login function.
+1. `./src/ui-components/SignOutButton.jsx` - Example of how to get the `PublicClientApplication` instance using the `useMsal` hook and invoking a logout function.
+1. `./src/utils/MsGraphApiCall.js` - Example of how to call the MS Graph API with an access token.
+1. `./src/utils/NavigationClient.js` - Example implementation of `INavigationClient` which can be used to override the default navigation functions MSAL.js uses
+
+### (Optional) MSAL React and class components
+
+For a demonstration of how to use MSAL React with class components, see: `./src/pages/ProfileWithMsal.jsx` and `./src/pages/ProfileRawContext.jsx`.
+
+*After* you initialize `MsalProvider`, there are 3 approaches you can take to protect your class components with MSAL React:
+
+1. Wrap each component that you want to protect with `withMsal` higher-order component (HOC) (e.g. [Profile](./src/pages/ProfileWithMsal.jsx#Profile)).
+1. Consume the raw context directly (e.g. [ProfileContent](./src/pages/ProfileRawContext.jsx#ProfileContent)).
+1. Pass context down from a parent component that has access to the `msalContext` via one of the other means above (e.g. [ProfileContent](./src/pages/ProfileWithMsal.jsx#ProfileContent)).
+
+For more information, visit:
+
+- [Docs: Class Components](../../../lib/msal-react/docs/class-components.md)
+- [MSAL React FAQ](../../../lib/msal-react/FAQ.md)
+
+## How to run the sample
+
+### Pre-requisites
+
+- Ensure [all pre-requisites](../../../lib/msal-react/README.md#prerequisites) have been completed to run `@azure/msal-react`.
+- Install node.js if needed ().
+
+### Configure the application
+
+- Open `./.env.development` in an editor.
+- Replace `ENTER_CLIENT_ID_HERE` with the Application (client) ID from the portal registration, or use the currently configured lab registration.
+- Replace `ENTER_TENANT_ID_HERE` with the tenant ID from the portal registration, or use the currently configured lab registration.
+ - Optionally, you may replace any of the other parameters, or you can remove them and use the default values.
+
+These parameters are used in `./src/authConfig.js` to configure MSAL.
+
+#### Install npm dependencies for sample
+
+```bash
+# Install dev dependencies for msal-react and msal-browser from root of repo
+npm install
+
+# Change directory to sample directory
+cd samples/msal-react-samples/react18-sample
+
+# Build packages locally
+npm run build:package
+```
+
+#### Running the sample development server
+
+1. In a command prompt, run `npm start`.
+1. Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
+1. Open [http://localhost:3000/profile](http://localhost:3000/profile) to see an example of a protected route. If you are not yet signed in, signin will be invoked automatically.
+
+The page will reload if you make edits.
+You will also see any lint errors in the console.
+
+- In the web page, click on the "Login" button and select either `Sign in using Popup` or `Sign in using Redirect` to begin the auth flow.
+
+#### Running the sample production server
+
+1. In a command prompt, run `npm run build`.
+1. Next run `npx vite preview --port 3000 --strictPort`.
+1. Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
+1. Open [http://localhost:3000/profile](http://localhost:3000/profile) to see an example of a protected route. If you are not yet signed in, signin will be invoked automatically.
+
+#### Learn more about the 3rd-party libraries used to create this sample
+
+- [React documentation](https://reactjs.org/).
+- [Vite documentation](https://vite.dev/guide/)
+- [React Router documentation](https://reactrouter.com/web/guides/quick-start)
+- [Material-UI documentation](https://material-ui.com/getting-started/installation/)
diff --git a/samples/msal-react-samples/react18-sample/index.html b/samples/msal-react-samples/react18-sample/index.html
new file mode 100644
index 0000000000..884d5dc1bd
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+ MSAL-React Sample
+
+
+
+
+
+
+
diff --git a/samples/msal-react-samples/react18-sample/jest.config.cjs b/samples/msal-react-samples/react18-sample/jest.config.cjs
new file mode 100644
index 0000000000..c6dedc82ed
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/jest.config.cjs
@@ -0,0 +1,8 @@
+module.exports = {
+ displayName: "React 18 Compat",
+ globals: {
+ __PORT__: 3000,
+ __STARTCMD__: "env-cmd -f .env.e2e npm start",
+ },
+ preset: "../../e2eTestUtils/jest-puppeteer-utils/jest-preset.js",
+};
diff --git a/samples/msal-react-samples/react18-sample/package.json b/samples/msal-react-samples/react18-sample/package.json
new file mode 100644
index 0000000000..fc8895633c
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "react18-sample",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "dependencies": {
+ "@azure/msal-browser": "^5.0.0",
+ "@azure/msal-react": "^5.0.0",
+ "@emotion/react": "^11.10.5",
+ "@emotion/styled": "^11.10.5",
+ "@mui/icons-material": "^5.10.16",
+ "@mui/material": "^5.10.17",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-router-dom": "^6.7.0"
+ },
+ "scripts": {
+ "start": "vite",
+ "test:e2e": "jest",
+ "build": "vite build",
+ "build:package": "cd ../../../ && npm run build:all --workspace=lib/msal-react"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "devDependencies": {
+ "@types/jest": "^29.5.0",
+ "@vitejs/plugin-react": "^4.3.3",
+ "e2e-test-utils": "file:../../e2eTestUtils",
+ "env-cmd": "^10.1.0",
+ "jest": "^29.5.0",
+ "jest-junit": "^16.0.0",
+ "ts-jest": "^29.1.0",
+ "vite": "^5.4.21"
+ },
+ "jest-junit": {
+ "suiteNameTemplate": "React 18 Compat Tests",
+ "outputDirectory": ".",
+ "outputName": "test-results.xml"
+ }
+}
\ No newline at end of file
diff --git a/samples/msal-react-samples/react18-sample/public/favicon.ico b/samples/msal-react-samples/react18-sample/public/favicon.ico
new file mode 100644
index 0000000000..a11777cc47
Binary files /dev/null and b/samples/msal-react-samples/react18-sample/public/favicon.ico differ
diff --git a/samples/msal-react-samples/react18-sample/src/App.jsx b/samples/msal-react-samples/react18-sample/src/App.jsx
new file mode 100644
index 0000000000..6756321e28
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/src/App.jsx
@@ -0,0 +1,62 @@
+import { Routes, Route, useNavigate, useLocation } from "react-router-dom";
+// Material-UI imports
+import Grid from "@mui/material/Grid";
+
+// MSAL imports
+import { MsalProvider } from "@azure/msal-react";
+import { CustomNavigationClient } from "./utils/NavigationClient";
+
+// Sample app imports
+import { PageLayout } from "./ui-components/PageLayout";
+import { Home } from "./pages/Home";
+import { Profile } from "./pages/Profile";
+import { Logout } from "./pages/Logout";
+import { Redirect } from "./pages/Redirect";
+
+// Class-based equivalents of "Profile" component
+import { ProfileWithMsal } from "./pages/ProfileWithMsal";
+import { ProfileRawContext } from "./pages/ProfileRawContext";
+import { ProfileUseMsalAuthenticationHook } from "./pages/ProfileUseMsalAuthenticationHook";
+
+function App({ pca }) {
+ // The next 3 lines are optional. This is how you configure MSAL to take advantage of the router's navigate functions when MSAL redirects between pages in your app
+ const navigate = useNavigate();
+ const location = useLocation();
+ const navigationClient = new CustomNavigationClient(navigate);
+ pca.setNavigationClient(navigationClient);
+
+ // Don't wrap redirect page in MsalProvider to prevent MSAL from consuming the auth response
+ const isRedirectPage = location.pathname === '/redirect';
+
+ if (isRedirectPage) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+function Pages() {
+ return (
+
+ } />
+ } />
+ } />
+ }
+ />
+ } />
+ } />
+
+ );
+}
+
+export default App;
diff --git a/samples/msal-react-samples/react18-sample/src/authConfig.js b/samples/msal-react-samples/react18-sample/src/authConfig.js
new file mode 100644
index 0000000000..73ea34a7f7
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/src/authConfig.js
@@ -0,0 +1,51 @@
+import { LogLevel, BrowserUtils } from "@azure/msal-browser";
+
+// Config object to be passed to Msal on creation
+export const msalConfig = {
+ auth: {
+ clientId: import.meta.env.VITE_CLIENT_ID,
+ authority: import.meta.env.VITE_AUTHORITY,
+ redirectUri: import.meta.env.VITE_REDIRECT_URI,
+ postLogoutRedirectUri: "/",
+ onRedirectNavigate: () => !BrowserUtils.isInIframe()
+ },
+ cache: {
+ cacheLocation: "localStorage",
+ },
+ system: {
+ allowPlatformBroker: false, // Disables WAM Broker
+ loggerOptions: {
+ loggerCallback: (level, message, containsPii) => {
+ if (containsPii) {
+ return;
+ }
+ switch (level) {
+ case LogLevel.Error:
+ console.error(message);
+ return;
+ case LogLevel.Info:
+ console.info(message);
+ return;
+ case LogLevel.Verbose:
+ console.debug(message);
+ return;
+ case LogLevel.Warning:
+ console.warn(message);
+ return;
+ default:
+ return;
+ }
+ },
+ },
+ },
+};
+
+// Add here scopes for id token to be used at MS Identity Platform endpoints.
+export const loginRequest = {
+ scopes: ["User.Read"]
+};
+
+// Add here the endpoints for MS Graph API services you would like to use.
+export const graphConfig = {
+ graphMeEndpoint: "https://graph.microsoft.com/v1.0/me"
+};
diff --git a/samples/msal-react-samples/react18-sample/src/index.jsx b/samples/msal-react-samples/react18-sample/src/index.jsx
new file mode 100644
index 0000000000..f75f9c6eac
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/src/index.jsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter as Router } from "react-router-dom";
+import { ThemeProvider } from "@mui/material/styles";
+import { theme } from "./styles/theme";
+import App from './App';
+
+// MSAL imports
+import { PublicClientApplication, EventType } from "@azure/msal-browser";
+import { msalConfig } from "./authConfig";
+
+export const msalInstance = new PublicClientApplication(msalConfig);
+
+msalInstance.initialize().then(() => {
+ // 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]);
+ }
+
+ msalInstance.addEventCallback((event) => {
+ if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) {
+ const account = event.payload;
+ msalInstance.setActiveAccount(account);
+ }
+ });
+
+ const container = document.getElementById("root");
+ const root = ReactDOM.createRoot(container);
+
+ root.render(
+
+
+
+
+
+ );
+});
diff --git a/samples/msal-react-samples/react18-sample/src/pages/Home.jsx b/samples/msal-react-samples/react18-sample/src/pages/Home.jsx
new file mode 100644
index 0000000000..288c3eb8bb
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/src/pages/Home.jsx
@@ -0,0 +1,26 @@
+import { AuthenticatedTemplate, UnauthenticatedTemplate } from "@azure/msal-react";
+import Button from "@mui/material/Button";
+import ButtonGroup from "@mui/material/ButtonGroup";
+import Typography from "@mui/material/Typography";
+import { Link as RouterLink } from "react-router-dom";
+
+export function Home() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
Please sign-in to see your profile information.
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/samples/msal-react-samples/react18-sample/src/pages/Logout.jsx b/samples/msal-react-samples/react18-sample/src/pages/Logout.jsx
new file mode 100644
index 0000000000..21d765b948
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/src/pages/Logout.jsx
@@ -0,0 +1,16 @@
+import React, { useEffect } from "react";
+import { useMsal } from "@azure/msal-react";
+
+export function Logout() {
+ const { instance } = useMsal();
+
+ useEffect(() => {
+ instance.logoutRedirect({
+ account: instance.getActiveAccount(),
+ })
+ }, [ instance ]);
+
+ return (
+
Logout
+ )
+}
diff --git a/samples/msal-react-samples/react18-sample/src/pages/Profile.jsx b/samples/msal-react-samples/react18-sample/src/pages/Profile.jsx
new file mode 100644
index 0000000000..73965b9448
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/src/pages/Profile.jsx
@@ -0,0 +1,76 @@
+import { useEffect, useState, useCallback } from "react";
+
+// Msal imports
+import { MsalAuthenticationTemplate, useMsal } from "@azure/msal-react";
+import { EventType, InteractionType, InteractionRequiredAuthError } from "@azure/msal-browser";
+import { loginRequest } from "../authConfig";
+
+// Sample app imports
+import { ProfileData } from "../ui-components/ProfileData";
+import { Loading } from "../ui-components/Loading";
+import { ErrorComponent } from "../ui-components/ErrorComponent";
+import { callMsGraph } from "../utils/MsGraphApiCall";
+
+// Material-ui imports
+import Paper from "@mui/material/Paper";
+
+const ProfileContent = () => {
+ const { instance } = useMsal();
+ const [graphData, setGraphData] = useState(null);
+
+ const fetchProfile = useCallback(() => {
+ if (!instance.getActiveAccount()) {
+ return;
+ }
+ callMsGraph().then(response => setGraphData(response)).catch((e) => {
+ if (e instanceof InteractionRequiredAuthError) {
+ instance.acquireTokenRedirect({
+ ...loginRequest,
+ account: instance.getActiveAccount()
+ });
+ }
+ });
+ }, [instance]);
+
+ useEffect(() => {
+ // Attempt to fetch profile data immediately
+ fetchProfile();
+
+ // Subscribe to active account changes so the Graph call is retried
+ // once setActiveAccount has been called.
+ const callbackId = instance.addEventCallback((event) => {
+ if (event.eventType === EventType.ACTIVE_ACCOUNT_CHANGED) {
+ fetchProfile();
+ }
+ });
+
+ return () => {
+ if (callbackId) {
+ instance.removeEventCallback(callbackId);
+ }
+ };
+ }, [instance, fetchProfile]);
+
+ return (
+
+ { graphData ? : null }
+
+ );
+};
+
+export function Profile() {
+ const authRequest = {
+ ...loginRequest
+ };
+
+ return (
+
+
+
+ )
+};
\ No newline at end of file
diff --git a/samples/msal-react-samples/react18-sample/src/pages/ProfileRawContext.jsx b/samples/msal-react-samples/react18-sample/src/pages/ProfileRawContext.jsx
new file mode 100644
index 0000000000..a6db93a520
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/src/pages/ProfileRawContext.jsx
@@ -0,0 +1,109 @@
+import { Component } from "react";
+
+// Msal imports
+import { MsalAuthenticationTemplate, MsalContext } from "@azure/msal-react";
+import { InteractionType, EventType, InteractionRequiredAuthError } from "@azure/msal-browser";
+import { loginRequest } from "../authConfig";
+
+// Sample app imports
+import { ProfileData } from "../ui-components/ProfileData";
+import { Loading } from "../ui-components/Loading";
+import { ErrorComponent } from "../ui-components/ErrorComponent";
+import { callMsGraph } from "../utils/MsGraphApiCall";
+
+// Material-ui imports
+import Paper from "@mui/material/Paper";
+
+
+/**
+ * This class is using the raw context directly. The available
+ * objects and methods are the same as in "withMsal" HOC usage.
+ */
+class ProfileContent extends Component {
+
+ static contextType = MsalContext;
+
+ constructor(props) {
+ super(props)
+
+ this.state = {
+ graphData: null,
+ }
+
+ this.callbackId = null;
+ }
+
+ fetchGraphData() {
+ if (this.state.graphData) {
+ return;
+ }
+
+ const instance = this.context.instance;
+ if (!instance.getActiveAccount()) {
+ return;
+ }
+
+ callMsGraph().then(response => this.setState({graphData: response})).catch((e) => {
+ if (e instanceof InteractionRequiredAuthError) {
+ instance.acquireTokenRedirect({
+ ...loginRequest,
+ account: instance.getActiveAccount()
+ });
+ }
+ });
+ }
+
+ componentDidMount() {
+ // Attempt to fetch profile data immediately
+ this.fetchGraphData();
+
+ // Subscribe to active account changes so the Graph call is retried
+ // once setActiveAccount has been called.
+ this.callbackId = this.context.instance.addEventCallback((event) => {
+ if (event.eventType === EventType.ACTIVE_ACCOUNT_CHANGED) {
+ this.fetchGraphData();
+ }
+ });
+ }
+
+ componentWillUnmount() {
+ if (this.callbackId) {
+ this.context.instance.removeEventCallback(this.callbackId);
+ }
+ }
+
+ render() {
+ return (
+
+ { this.state.graphData ? : null }
+
+ );
+ }
+}
+
+/**
+ * This class is using "withMsal" HOC. It passes down the msalContext
+ * as a prop to its children.
+ */
+class Profile extends Component {
+
+ render() {
+
+ const authRequest = {
+ ...loginRequest
+ };
+
+ return (
+
+
+
+ );
+ }
+}
+
+export const ProfileRawContext = Profile
diff --git a/samples/msal-react-samples/react18-sample/src/pages/ProfileUseMsalAuthenticationHook.jsx b/samples/msal-react-samples/react18-sample/src/pages/ProfileUseMsalAuthenticationHook.jsx
new file mode 100644
index 0000000000..43bdf3fe11
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/src/pages/ProfileUseMsalAuthenticationHook.jsx
@@ -0,0 +1,51 @@
+import { useEffect, useState } from "react";
+
+// Msal imports
+import { useMsalAuthentication } from "@azure/msal-react";
+import { InteractionType } from "@azure/msal-browser";
+import { loginRequest } from "../authConfig";
+
+// Sample app imports
+import { ProfileData } from "../ui-components/ProfileData";
+import { ErrorComponent } from "../ui-components/ErrorComponent";
+import { callMsGraph } from "../utils/MsGraphApiCall";
+
+// Material-ui imports
+import Paper from "@mui/material/Paper";
+
+const ProfileContent = () => {
+ const [graphData, setGraphData] = useState(null);
+ const { result, error } = useMsalAuthentication(InteractionType.Popup, {
+ ...loginRequest,
+ });
+
+ useEffect(() => {
+ if (!!graphData) {
+ // We already have the data, no need to call the API
+ return;
+ }
+
+ if (!!error) {
+ // Error occurred attempting to acquire a token, either handle the error or do nothing
+ return;
+ }
+
+ if (result) {
+ callMsGraph().then(response => setGraphData(response));
+ }
+ }, [error, result, graphData]);
+
+ if (error) {
+ return ;
+ }
+
+ return (
+
+ { graphData ? : null }
+
+ );
+};
+
+export function ProfileUseMsalAuthenticationHook() {
+ return
+};
diff --git a/samples/msal-react-samples/react18-sample/src/pages/ProfileWithMsal.jsx b/samples/msal-react-samples/react18-sample/src/pages/ProfileWithMsal.jsx
new file mode 100644
index 0000000000..1a2b0b0335
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/src/pages/ProfileWithMsal.jsx
@@ -0,0 +1,87 @@
+import { Component } from "react";
+
+// Msal imports
+import { MsalAuthenticationTemplate, withMsal } from "@azure/msal-react";
+import { InteractionType, InteractionStatus, InteractionRequiredAuthError } from "@azure/msal-browser";
+import { loginRequest } from "../authConfig";
+
+// Sample app imports
+import { ProfileData } from "../ui-components/ProfileData";
+import { Loading } from "../ui-components/Loading";
+import { ErrorComponent } from "../ui-components/ErrorComponent";
+import { callMsGraph } from "../utils/MsGraphApiCall";
+
+// Material-ui imports
+import Paper from "@mui/material/Paper";
+
+/**
+ * This class is a child component of "Profile". MsalContext is passed
+ * down from the parent and available as a prop here.
+ */
+class ProfileContent extends Component {
+
+ constructor(props) {
+ super(props)
+
+ this.state = {
+ graphData: null,
+ }
+ }
+
+ setGraphData() {
+ if (!this.state.graphData && this.props.msalContext.inProgress === InteractionStatus.None) {
+ callMsGraph().then(response => this.setState({graphData: response})).catch((e) => {
+ if (e instanceof InteractionRequiredAuthError) {
+ this.props.msalContext.instance.acquireTokenRedirect({
+ ...loginRequest,
+ account: this.props.msalContext.instance.getActiveAccount()
+ });
+ }
+ });
+ }
+ }
+
+ componentDidMount() {
+ this.setGraphData();
+ }
+
+ componentDidUpdate() {
+ this.setGraphData();
+ }
+
+ render() {
+ return (
+
+ { this.state.graphData ? : null }
+
+ );
+ }
+}
+
+/**
+ * This class is using "withMsal" HOC and has access to authentication
+ * state. It passes down the msalContext as a prop to its children.
+ */
+class Profile extends Component {
+
+ render() {
+
+ const authRequest = {
+ ...loginRequest
+ };
+
+ return (
+
+
+
+ );
+ }
+}
+
+// Wrap your class component to access authentication state as props
+export const ProfileWithMsal = withMsal(Profile);
\ No newline at end of file
diff --git a/samples/msal-react-samples/react18-sample/src/pages/Redirect.jsx b/samples/msal-react-samples/react18-sample/src/pages/Redirect.jsx
new file mode 100644
index 0000000000..dd4529aaf3
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/src/pages/Redirect.jsx
@@ -0,0 +1,20 @@
+// This page serves the redirect bridge for MSAL authentication
+// It's excluded from MsalProvider wrapper in App.js to prevent MSAL from processing the hash
+import { useEffect } from "react";
+import { broadcastResponseToMainFrame } from "@azure/msal-browser/redirect-bridge";
+
+export function Redirect() {
+ useEffect(() => {
+ // Call broadcastResponseToMainFrame when component mounts
+ broadcastResponseToMainFrame().catch((error) => {
+ console.error("Error broadcasting response to main frame:", error);
+ });
+ }, []);
+
+ return (
+
+
Processing authentication...
+
+ );
+}
+
diff --git a/samples/msal-react-samples/react18-sample/src/styles/theme.js b/samples/msal-react-samples/react18-sample/src/styles/theme.js
new file mode 100644
index 0000000000..442acc8121
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/src/styles/theme.js
@@ -0,0 +1,20 @@
+import { unstable_createMuiStrictModeTheme as createMuiTheme } from '@mui/material/styles';
+import { red } from '@mui/material/colors';
+
+// Create a theme instance.
+export const theme = createMuiTheme({
+ palette: {
+ primary: {
+ main: '#556cd6',
+ },
+ secondary: {
+ main: '#19857b',
+ },
+ error: {
+ main: red.A400,
+ },
+ background: {
+ default: '#fff',
+ },
+ },
+});
diff --git a/samples/msal-react-samples/react18-sample/src/ui-components/AccountPicker.jsx b/samples/msal-react-samples/react18-sample/src/ui-components/AccountPicker.jsx
new file mode 100644
index 0000000000..6b1f855fe3
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/src/ui-components/AccountPicker.jsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import { useMsal } from "@azure/msal-react";
+import Avatar from '@mui/material/Avatar';
+import List from '@mui/material/List';
+import ListItem from '@mui/material/ListItem';
+import ListItemAvatar from '@mui/material/ListItemAvatar';
+import ListItemText from '@mui/material/ListItemText';
+import DialogTitle from '@mui/material/DialogTitle';
+import Dialog from '@mui/material/Dialog';
+import PersonIcon from '@mui/icons-material/Person';
+import AddIcon from '@mui/icons-material/Add';
+import { loginRequest } from "../authConfig";
+
+export const AccountPicker = (props) => {
+ const { instance, accounts } = useMsal();
+ const { onClose, open } = props;
+
+ const handleListItemClick = (account) => {
+ instance.setActiveAccount(account);
+ if (!account) {
+ instance.loginRedirect({
+ ...loginRequest,
+ prompt: "login"
+ })
+ } else {
+ // To ensure account related page attributes update after the account is changed
+ window.location.reload();
+ }
+
+ onClose(account);
+ };
+
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/samples/msal-react-samples/react18-sample/src/ui-components/ErrorComponent.jsx b/samples/msal-react-samples/react18-sample/src/ui-components/ErrorComponent.jsx
new file mode 100644
index 0000000000..de8ddd2607
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/src/ui-components/ErrorComponent.jsx
@@ -0,0 +1,5 @@
+import { Typography } from "@mui/material";
+
+export const ErrorComponent = ({error}) => {
+ return An Error Occurred: {error.errorCode};
+}
\ No newline at end of file
diff --git a/samples/msal-react-samples/react18-sample/src/ui-components/Loading.jsx b/samples/msal-react-samples/react18-sample/src/ui-components/Loading.jsx
new file mode 100644
index 0000000000..c2ae494c80
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/src/ui-components/Loading.jsx
@@ -0,0 +1,5 @@
+import { Typography } from "@mui/material";
+
+export const Loading = () => {
+ return Authentication in progress...
+}
\ No newline at end of file
diff --git a/samples/msal-react-samples/react18-sample/src/ui-components/NavBar.jsx b/samples/msal-react-samples/react18-sample/src/ui-components/NavBar.jsx
new file mode 100644
index 0000000000..bea9940fbc
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/src/ui-components/NavBar.jsx
@@ -0,0 +1,25 @@
+import AppBar from "@mui/material/AppBar";
+import Toolbar from "@mui/material/Toolbar";
+import Link from "@mui/material/Link";
+import Typography from "@mui/material/Typography";
+import WelcomeName from "./WelcomeName";
+import SignInSignOutButton from "./SignInSignOutButton";
+import { Link as RouterLink } from "react-router-dom";
+
+const NavBar = () => {
+ return (
+
+
+
+
+ MS Identity Platform
+
+
+
+
+
+
+ );
+};
+
+export default NavBar;
\ No newline at end of file
diff --git a/samples/msal-react-samples/react18-sample/src/ui-components/PageLayout.jsx b/samples/msal-react-samples/react18-sample/src/ui-components/PageLayout.jsx
new file mode 100644
index 0000000000..f4e5672879
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/src/ui-components/PageLayout.jsx
@@ -0,0 +1,16 @@
+import Typography from "@mui/material/Typography";
+import NavBar from "./NavBar";
+
+export const PageLayout = (props) => {
+ return (
+ <>
+
+
+
Welcome to the Microsoft Authentication Library For React Quickstart
+
+
+
+ {props.children}
+ >
+ );
+};
\ No newline at end of file
diff --git a/samples/msal-react-samples/react18-sample/src/ui-components/ProfileData.jsx b/samples/msal-react-samples/react18-sample/src/ui-components/ProfileData.jsx
new file mode 100644
index 0000000000..f7aa6223c7
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/src/ui-components/ProfileData.jsx
@@ -0,0 +1,78 @@
+import React from "react";
+import List from "@mui/material/List";
+import ListItem from "@mui/material/ListItem";
+import ListItemText from "@mui/material/ListItemText";
+import ListItemAvatar from "@mui/material/ListItemAvatar";
+import Avatar from "@mui/material/Avatar";
+import PersonIcon from '@mui/icons-material/Person';
+import WorkIcon from "@mui/icons-material/Work";
+import MailIcon from '@mui/icons-material/Mail';
+import PhoneIcon from '@mui/icons-material/Phone';
+import LocationOnIcon from '@mui/icons-material/LocationOn';
+
+export const ProfileData = ({graphData}) => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+const NameListItem = ({name}) => (
+
+
+
+
+
+
+
+
+);
+
+const JobTitleListItem = ({jobTitle}) => (
+
+
+
+
+
+
+
+
+);
+
+const MailListItem = ({mail}) => (
+
+
+
+
+
+
+
+
+);
+
+const PhoneListItem = ({phone}) => (
+
+
+
+
+
+
+
+
+);
+
+const LocationListItem = ({location}) => (
+
+
+
+
+
+
+
+
+);
diff --git a/samples/msal-react-samples/react18-sample/src/ui-components/SignInButton.jsx b/samples/msal-react-samples/react18-sample/src/ui-components/SignInButton.jsx
new file mode 100644
index 0000000000..521b50ca7d
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/src/ui-components/SignInButton.jsx
@@ -0,0 +1,149 @@
+import { useState } from "react";
+import { useMsal } from "@azure/msal-react";
+import Button from "@mui/material/Button";
+import MenuItem from '@mui/material/MenuItem';
+import Menu from '@mui/material/Menu';
+import Alert from '@mui/material/Alert';
+import AlertTitle from '@mui/material/AlertTitle';
+import Dialog from '@mui/material/Dialog';
+import DialogActions from '@mui/material/DialogActions';
+import DialogContent from '@mui/material/DialogContent';
+import DialogContentText from '@mui/material/DialogContentText';
+import DialogTitle from '@mui/material/DialogTitle';
+import { loginRequest } from "../authConfig";
+
+export const SignInButton = () => {
+ const { instance } = useMsal();
+
+ const [anchorEl, setAnchorEl] = useState(null);
+ const [showRetryDialog, setShowRetryDialog] = useState(false);
+ const [retryRequested, setRetryRequested] = useState(false);
+ const [showPopupWarning, setShowPopupWarning] = useState(false);
+ const open = Boolean(anchorEl);
+
+ const handleLogin = async (loginType) => {
+ setAnchorEl(null);
+
+ if (loginType === "popup") {
+ // Show warning when popup is about to open
+ setShowPopupWarning(true);
+
+ try {
+ await instance.loginPopup({
+ ...loginRequest,
+ // Only override if user explicitly clicked retry
+ overrideInteractionInProgress: retryRequested
+ });
+
+ // Hide warning on success
+ setShowPopupWarning(false);
+ setRetryRequested(false);
+ } catch (error) {
+ // Hide warning on error
+ setShowPopupWarning(false);
+
+ if (error.errorCode === 'interaction_in_progress') {
+ // Show retry dialog - let user decide whether to retry
+ setShowRetryDialog(true);
+ } else {
+ // Reset retry flag for other errors
+ setRetryRequested(false);
+ console.error(error);
+ }
+ }
+ } else if (loginType === "redirect") {
+ instance.loginRedirect(loginRequest);
+ }
+ }
+
+ const handleRetry = () => {
+ setShowRetryDialog(false);
+ setRetryRequested(true); // User explicitly requested retry
+ handleLogin("popup");
+ }
+
+ const handleCancelRetry = () => {
+ setShowRetryDialog(false);
+ setRetryRequested(false);
+ }
+
+ return (
+
+
+
+
+ {/* Warning message during popup authentication */}
+ {showPopupWarning && (
+
+ Authentication in Progress
+ Please complete authentication in the popup window. Do not close the popup until authentication is complete.
+
+ )}
+
+ {/* Retry dialog for interaction_in_progress errors */}
+
+
+ )
+};
diff --git a/samples/msal-react-samples/react18-sample/src/ui-components/SignInSignOutButton.jsx b/samples/msal-react-samples/react18-sample/src/ui-components/SignInSignOutButton.jsx
new file mode 100644
index 0000000000..61633e1459
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/src/ui-components/SignInSignOutButton.jsx
@@ -0,0 +1,20 @@
+import { useIsAuthenticated, useMsal } from "@azure/msal-react";
+import { SignInButton } from "./SignInButton";
+import { SignOutButton } from "./SignOutButton";
+import { InteractionStatus } from "@azure/msal-browser";
+
+const SignInSignOutButton = () => {
+ const { inProgress } = useMsal();
+ const isAuthenticated = useIsAuthenticated();
+
+ if (isAuthenticated) {
+ return ;
+ } else if (inProgress !== InteractionStatus.Startup && inProgress !== InteractionStatus.HandleRedirect) {
+ // inProgress check prevents sign-in button from being displayed briefly after returning from a redirect sign-in. Processing the server response takes a render cycle or two
+ return ;
+ } else {
+ return null;
+ }
+}
+
+export default SignInSignOutButton;
\ No newline at end of file
diff --git a/samples/msal-react-samples/react18-sample/src/ui-components/SignOutButton.jsx b/samples/msal-react-samples/react18-sample/src/ui-components/SignOutButton.jsx
new file mode 100644
index 0000000000..2a9fbd451e
--- /dev/null
+++ b/samples/msal-react-samples/react18-sample/src/ui-components/SignOutButton.jsx
@@ -0,0 +1,65 @@
+import { useState } from "react";
+import { useMsal } from "@azure/msal-react";
+import IconButton from '@mui/material/IconButton';
+import AccountCircle from "@mui/icons-material/AccountCircle";
+import MenuItem from '@mui/material/MenuItem';
+import Menu from '@mui/material/Menu';
+import { AccountPicker } from "./AccountPicker";
+
+export const SignOutButton = () => {
+ const { instance } = useMsal();
+ const [accountSelectorOpen, setOpen] = useState(false);
+
+ const [anchorEl, setAnchorEl] = useState(null);
+ const open = Boolean(anchorEl);
+
+ const handleLogout = (logoutType) => {
+ setAnchorEl(null);
+
+ if (logoutType === "popup") {
+ instance.logoutPopup();
+ } else if (logoutType === "redirect") {
+ instance.logoutRedirect();
+ }
+ }
+
+ const handleAccountSelection = () => {
+ setAnchorEl(null);
+ setOpen(true);
+ }
+
+ const handleClose = () => {
+ setOpen(false);
+ };
+
+ return (
+