/* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import React, { useEffect, useReducer, PropsWithChildren, useMemo, } from "react"; import { IPublicClientApplication, EventMessage, EventMessageUtils, InteractionStatus, Logger, WrapperSKU, AccountInfo, } from "@azure/msal-browser"; import { MsalContext, IMsalContext } from "./MsalContext.js"; import { accountArraysAreEqual } from "./utils/utilities.js"; import { name as SKU, version } from "./packageMetadata.js"; export type MsalProviderProps = PropsWithChildren<{ instance: IPublicClientApplication; }>; type MsalState = { inProgress: InteractionStatus; accounts: AccountInfo[]; }; const MsalProviderActionType = { UNBLOCK_INPROGRESS: "UNBLOCK_INPROGRESS", EVENT: "EVENT", } as const; type MsalProviderActionType = (typeof MsalProviderActionType)[keyof typeof MsalProviderActionType]; type MsalProviderReducerAction = { type: MsalProviderActionType; payload: { logger: Logger; instance: IPublicClientApplication; message?: EventMessage; }; }; /** * Returns the next inProgress and accounts state based on event message * @param previousState * @param action */ const reducer = ( previousState: MsalState, action: MsalProviderReducerAction ): MsalState => { const { type, payload } = action; let newInProgress = previousState.inProgress; switch (type) { case MsalProviderActionType.UNBLOCK_INPROGRESS: if (previousState.inProgress === InteractionStatus.Startup) { newInProgress = InteractionStatus.None; payload.logger.info( "MsalProvider - handleRedirectPromise resolved, setting inProgress to 'none'", "" ); } break; case MsalProviderActionType.EVENT: const message = payload.message as EventMessage; const status = EventMessageUtils.getInteractionStatusFromEvent( message, previousState.inProgress ); if (status) { payload.logger.info( `MsalProvider - '${message.eventType}' results in setting inProgress from '${previousState.inProgress}' to '${status}'`, "" ); newInProgress = status; } break; default: throw new Error(`Unknown action type: ${type}`); } if (newInProgress === InteractionStatus.Startup) { // Can't start checking accounts until initialization is complete return previousState; } const currentAccounts = payload.instance.getAllAccounts(); if ( newInProgress !== previousState.inProgress && !accountArraysAreEqual(currentAccounts, previousState.accounts) ) { // Both inProgress and accounts changed return { ...previousState, inProgress: newInProgress, accounts: currentAccounts, }; } else if (newInProgress !== previousState.inProgress) { // Only only inProgress changed return { ...previousState, inProgress: newInProgress, }; } else if ( !accountArraysAreEqual(currentAccounts, previousState.accounts) ) { // Only accounts changed return { ...previousState, accounts: currentAccounts, }; } else { // Nothing changed return previousState; } }; /** * MSAL context provider component. This must be rendered above any other components that use MSAL. */ export function MsalProvider({ instance, children, }: MsalProviderProps): React.ReactElement { useEffect(() => { instance.initializeWrapperLibrary(WrapperSKU.React, version); }, [instance]); // Create a logger instance for msal-react with the same options as PublicClientApplication const logger = useMemo(() => { return instance.getLogger().clone(SKU, version); }, [instance]); const [state, updateState] = useReducer(reducer, undefined, () => { // Lazy initialization of the initial state return { inProgress: InteractionStatus.Startup, accounts: [], }; }); useEffect(() => { const callbackId = instance.addEventCallback( (message: EventMessage) => { updateState({ payload: { instance, logger, message, }, type: MsalProviderActionType.EVENT, }); } ); logger.verbose( `MsalProvider - Registered event callback with id: '${callbackId}'`, "" ); instance .initialize() .then(() => { instance .handleRedirectPromise() .catch(() => { // Errors should be handled by listening to the LOGIN_FAILURE event return; }) .finally(() => { /* * If handleRedirectPromise returns a cached promise the necessary events may not be fired * This is a fallback to prevent inProgress from getting stuck in 'startup' */ updateState({ payload: { instance, logger, }, type: MsalProviderActionType.UNBLOCK_INPROGRESS, }); }); }) .catch(() => { // Errors should be handled by listening to the LOGIN_FAILURE event return; }); return () => { // Remove callback when component unmounts or accounts change if (callbackId) { logger.verbose( `MsalProvider - Removing event callback '${callbackId}'`, "" ); instance.removeEventCallback(callbackId); } }; }, [instance, logger]); const contextValue: IMsalContext = { instance, inProgress: state.inProgress, accounts: state.accounts, logger, }; return ( {children} ); }