---
title: Files
slug: experience/files
docTags: U4gHdNebGGoMU4rUtx-Hw,z8-QaXm-4AA8vr6q10eP9,Xogy-nAy3ZW4ooGTDDhXc
createdAt: 2025-12-10T13:01:45.869Z
---

## File Structure

:::BlockQuote
default/storefront/assets/scripts/custom/
├── custom.js                      # Main entry point - import init.js here
└── sso-manager/
&#x20;   ├── config.js                  # Configuration file
&#x20;   ├── sso-manager.js             # Core SSOManager class
&#x20;   └── init.js                    # Extended class with lifecycle hooks
:::

## Files

:::CodeblockTabs
config.js

```javascript
/**
 * Configuration for SSO Manager
 * @type {Config}
 */
export const config = {
  type: "oAuth", // Note: Currently unused in the class implementation
  withPostMessage: false,
  externalState: {
    login: ["logged_in", "true"],
    logout: ["logged_in", "false"],
    purchase: ["purchase", "true"],
    token: ["transferToken"]
  },
  urlParamKeys: {
    returnUrl: "returnTo",
    loginViaUrl: "external_login",
    checkoutViaUrl: "external_checkout",
    logoutViaUrl: "external_logout",
  },
  postMessage: {
    validOrigins: [
      "https://dev.hz.de",
      "https://web.purplemanager.com/heidenheimer-staging",
      "resource://dynamic",
    ],
  },
  targetUrlParams: {},
  returnUrlParams: {},
  removeUrlParams: ["jwt"],
  loginUrl: "https://checkout-stage.hz.de/dispatch",
  logoutUrl: "https://checkout-stage.hz.de/logout",
  checkoutUrl: "https://checkout-stage.hz.de/start",
};

/*
type ExternalStateKey = string; // url parameter key
type ExternalStateValue = string | undefined; // url parameter value
type ExternalState = [ExternalStateKey, ExternalStateValue]; // missing ExternalStateValue causes checks for key only

type Config = {
  type: 'oAuth' | 'transferToken'; // which sso procedure is active
  withPostMessage?: boolean; // enable triggering login/logout via post message?
  externalState: {
    login: ExternalState; // external login state
    logout: ExternalState; // external logout state
    purchase: ExternalState; // external purchase action
    token: ExternalState; // transferToken, usually declared without ExternalStateValue
  }
  urlParamKeys: {
    returnUrl: string; // under which url Parameter expects the external API the return url
    loginViaUrl: "external_login"; // url param which triggers the login, can receive a string as value which will be used as targetUrl
    checkoutViaUrl: "external_checkout"; // url param which triggers the checkout, can receive a string as value which will be used as targetUrl
    logoutViaUrl: "external_logout"; // url param which triggers the logout
  }
  postMessage: {
    validOrigins: string[]; // array of origins which are allowed to trigger SSOManager login/logout via post message
  }
  targetUrlParams: Record<string, string | number | boolean>; // additional params we need/want to add to the target url
  returnUrlParams: Record<string, string | number | boolean>; // additional params we need/want to add to the return url
  removeUrlParams: string[], // additional url params we want to remove upon login/logout (eg. to avoid endless login-loop)
  loginUrl: string; // url to be called to login externally when none is given in the login call
  logoutUrl: string; // url to be called to logout (always internally)
  checkoutUrl: string; // url to be called to checkout externally when none is given in the checkout call
}
*/

```

sso-manager.js

```javascript
import { config as ssoManagerConfig } from './config';

/**
 * SSOManager class handles external authentication flows for both web and app platforms.
 * Supports OAuth and token-based authentication, with optional postMessage integration for iframe communication.
 *
 * This is a singleton class - only one instance can exist at a time.
 *
 * Features:
 * - Login/logout via URL parameters
 * - Login/logout via postMessage (for iframe integration)
 * - Automatic state detection and synchronization
 * - Platform-specific handling (web vs app)
 * - Configurable return URLs and URL parameters
 *
 * @class
 */
export const SSOManager = class {

    static instance = null;

    isWeb;
    config = ssoManagerConfig;
    _customReturnUrl = undefined;

    /**
     * Initialize SSOManager instance
     * Sets up authentication flow based on configuration and initializes postMessage listener if enabled
     *
     * Singleton pattern: Returns existing instance if one already exists
     *
     * @constructor
     */
    constructor() {
        // Return existing instance if it exists
        if (SSOManager.instance) {
            return SSOManager.instance;
        }

        if (!this.config) {
            console.error('No ssoManagerConfig found.');
            // TODO: validation of config?
            return;
        }

        // Store instance
        SSOManager.instance = this;

        this.init();
        if (this.config.withPostMessage) {
            this.addPostMessage();
        }
    }

    /**
     * Get the singleton instance
     * Creates a new instance if none exists
     *
     * @returns {SSOManager} The singleton instance
     */
    static getInstance() {
        if (!SSOManager.instance) {
            SSOManager.instance = new SSOManager();
        }
        return SSOManager.instance;
    }

    /**
     * Reset the singleton instance
     * Useful for testing or reinitialization
     *
     * @returns {void}
     */
    static resetInstance() {
        SSOManager.instance = null;
    }

    /**
     * Initialize the authentication flow
     * - Detects platform (web/app)
     * - Exposes public methods
     * - Checks URL parameters for login/logout triggers
     * - Handles return from external SSO provider (web only)
     *
     * type Init = async () => void;
     */
    init = async () => {
        console.log('SSOManager.init config', this.config);

        const metadata = await purple.metadata.getMetadata();
        this.isWeb = metadata.platform === 'web';

        const url = new URL(window.location.href);

        const params = new URLSearchParams(url.search);
        const { loginViaUrl, checkoutViaUrl, logoutViaUrl } = this.config?.urlParamKeys ?? {};

        // trigger login via url param (eg. by app-menu)
        if (params.has(loginViaUrl)) {
            this.utils.clearUrlParams();
            this.login(url.searchParams.get(loginViaUrl));
        }
        // trigger checkout via url param (eg. by app-menu)
        else if (params.has(checkoutViaUrl)) {
            this.utils.clearUrlParams();
            this.checkout(url.searchParams.get(checkoutViaUrl));
        }
        // trigger logout via url param (eg. by app-menu)
        else if (params.has(logoutViaUrl)) {
            this.utils.clearUrlParams();
            this.logout();
        }
        // do web specific stuff upon init
        else if (this.isWeb) {
            this.initWeb(params);
        }
    };

    /**
     * Handle web-specific initialization
     * Upon initial page render, we might be returning from an external SSO provider.
     * This triggers the login/logout flow which decides if we need to login/logout.
     *
     * type InitWeb = (params: URLSearchParams) => void;
     */
    initWeb = (params) => {
        console.debug('SSOManager.initWeb params', params);

        const urlParams = Object.fromEntries(params);
        const sanitizedUrlParams = this.utils.sanitizeUrlParams(urlParams)

        console.debug('SSOManager.initWeb sanitizedUrlParams', sanitizedUrlParams);

        this.finishLogInOut(sanitizedUrlParams);
    }

    /**
     * Activate login/checkout/logout flow via postMessage
     * Used for paywalls in iframes and cross-origin communication
     *
     * type AddPostMessage = () => void;
     *
     * @example
     * // Post message structure:
     * type PostMessage = {
     *     method: 'login' | 'checkout' | 'logout';
     *     targetUrl?: string;
     * }
     *
     * // Usage examples:
     * const postMessage = {method: 'login', targetUrl: 'https://www.sso-provider.com/login'};
     * const postMessage = {method: 'login'}; // targetUrl optional, uses config
     * const postMessage = {method: 'checkout', targetUrl: 'https://www.sso-provider.com/checkout'};
     * const postMessage = {method: 'checkout'}; // targetUrl optional, uses config
     * const postMessage = {method: 'logout'}; // logout always ignores targetUrl
     * window.postMessage(postMessage)
     */
    addPostMessage = () => {
        window.addEventListener(
            "message",
            (event) => {
                console.debug('SSOManager.addPostMessage event.data', event.data);

                const receivedUrl = new URL(event.origin);
                const receivedOrigin = receivedUrl.origin;

                // Build list of valid origins with special handling for non-standard protocols
                const validOriginChecks = (this.config.postMessage?.validOrigins ?? []).map(configuredOrigin => {
                    const configUrl = new URL(configuredOrigin);
                    return {
                        origin: configUrl.origin,
                        protocol: configUrl.protocol,
                        hostname: configUrl.hostname,
                    };
                });

                // Check if received origin is valid
                // For standard origins (http/https), compare origin property
                // For non-standard protocols with origin="null", compare protocol+hostname
                const isValidOrigin = validOriginChecks.some(valid => {
                    if (valid.origin !== "null" && receivedOrigin !== "null") {
                        // Standard origin comparison (http/https)
                        return valid.origin === receivedOrigin;
                    } else if (valid.origin === "null" && receivedOrigin === "null") {
                        // Non-standard protocol comparison (resource://, etc.)
                        return valid.protocol === receivedUrl.protocol &&
                            valid.hostname === receivedUrl.hostname;
                    }
                    return false;
                });

                if (!isValidOrigin) {
                    console.debug('SSOManager.addPostMessage origin not in validOrigins:', receivedOrigin);
                    return;
                }

                console.debug('SSOManager.addPostMessage ', receivedOrigin, event.data);

                if (event.data.method === 'login') {
                    this.login(event.data.targetUrl);
                } else if (event.data.method === 'checkout') {
                    this.checkout(event.data.targetUrl);
                } else if (event.data.method === 'logout') {
                    this.logout();
                }
            },
            false,
        );
    }

    /**
     * Check if a given key/value pair matches externalState config
     * Returns true if the value matches, undefined if no decision can be made
     *
     * type CheckExternalState = (key: string, value?: string) => boolean | undefined;
     */
    checkExternalState(key, value) {
        console.debug('SSOManager.checkExternalState', key, value);

        // no value - no decision
        if (value == null) {
            return;
        }

        // if the given key is not configured - no decision
        if (!this.config.externalState[key]?.[0]) {
            console.error(`No configuration found for SSOManager.config.externalState[${key}]`);
            return;
        }

        // if we leave the second entry in externalState[key] config empty means any value is valid
        if (!this.config.externalState[key][1]) {
            return true
        }

        // check if the value matches the expected value
        return this.config.externalState[key][1] === value;
    }

    /**
     * Decide if we need to (re-)login in the app
     * according to information we got from SSO provider
     *
     * type LoginRequired = ({
     *  token?: boolean;
     *  login?: boolean;
     *  purchase?: boolean;
     * }) => Promise<boolean>;
     */
    loginRequired = async ({ token, login, purchase }) => {
        console.debug('SSOManager.loginRequired', { token, login, purchase });
        // when token or purchase we have to (re-)login regardless the local login state
        if (purchase || token) {
            console.debug('SSOManager.loginRequired purchase || token');
            return true;
        }
        if (!login) {
            return false;
        }
        // otherwise we check if we are logged in externally
        const userData = await purple.entitlement.getUserData();
        return !userData.accessToken;
    }

    /**
     * Decide if we need to logout in the app
     * according to information we got from SSO provider
     *
     * type LogoutRequired = ({
     *  logout?: boolean | undefined;
     * }) => Promise<boolean>;
     */
    logoutRequired = async ({ logout }) => {
        console.debug('SSOManager.logoutRequired', { logout });
        if (!logout) return false;
        const userData = await purple.entitlement.getUserData();
        return !!userData.accessToken;
    }

    /**
     * Start authentication process in native apps
     * Uses purple.app.performAuthentication to handle the OAuth flow
     *
     * type AppAuthentication = (targetUrl: string) => void
     */
    appAuthentication = (targetUrl) => {
        purple.app
            .performAuthentication({
                url: targetUrl,
                // use whatever key is defined as returnUrl key
                callbackParamName: this.config?.urlParamKeys?.returnUrl,
            })
            .then(returnedData => {
                const sanitizedUrlParams = this.utils.sanitizeUrlParams(returnedData.values)

                console.debug('SSOManager.appAuthentication sanitizedUrlParams', returnedData, sanitizedUrlParams);

                this.finishLogInOut(sanitizedUrlParams);
            })
            .catch(error => this.loginError(error));
    }

    /**
     * Trigger the login flow
     *
     * @param {string} [targetUrl] - SSO login URL. Uses config.loginUrl if omitted
     * @param {Object} [options] - Additional options
     * @param {Array<[string, string|number]>} [options.params] - URL parameters to add as [key, value] pairs
     * @param {string} [options.returnUrl] - Custom return URL for this specific login call
     * @returns {void}
     */
    login = (targetUrl, options = {}) => {
        console.debug('SSOManager.login targetUrl', targetUrl);
        if (!targetUrl) {
            targetUrl = this.config.loginUrl;
        }
        if (!targetUrl) {
            console.error('SSOManager.login: No targetUrl found!');
            return;
        }

        const { params, returnUrl } = options;

        // sets a customReturnUrl which will be used later
        // only useful for web
        if (returnUrl) {
            this._customReturnUrl = returnUrl;
        }

        const url = new URL(targetUrl);

        if (Array.isArray(params)) {
            params.map(_params => {
                url.searchParams.append(..._params);
            })
        }

        console.debug('SSOManager.login url', url, url.toString());

        if (this.isWeb) {
            this.loginWeb(url.toString());
        } else {
            this.loginApp(url.toString());
        }
    };

    /**
     * Trigger the checkout flow
     * Same signature as login()
     *
     * @param {string} [targetUrl] - SSO checkout URL. Uses config.checkoutUrl if omitted
     * @param {Object} [options] - Additional options (same as login)
     * @param {Array<[string, string|number]>} [options.params] - URL parameters to add
     * @param {string} [options.returnUrl] - Custom return URL
     * @returns {void}
     */
    checkout = (targetUrl, options = {}) => {
        console.debug('SSOManager.checkout targetUrl', targetUrl);
        if (!targetUrl) {
            targetUrl = this.config.checkoutUrl;
        }
        if (!targetUrl) {
            console.error('SSOManager.checkout: No targetUrl found!');
            return;
        }
        this.login(targetUrl, options);
    }

    /**
     * Handle login flow for native apps
     *
     * @param {string} targetUrl - The SSO URL to authenticate against
     * @returns {void}
     */
    loginApp = (targetUrl) => {
        console.debug('SSOManager.loginApp targetUrl', targetUrl);
        this.appAuthentication(this.utils.handleTargetUrl(targetUrl, false));
    }

    /**
     * Handle login flow for web platform
     * Redirects browser to SSO provider
     *
     * @param {string} targetUrl - The SSO URL to authenticate against
     * @returns {void}
     */
    loginWeb = (targetUrl) => {
        console.debug('SSOManager.loginWeb targetUrl', targetUrl);
        targetUrl = this.utils.handleTargetUrl(targetUrl);
        console.debug('SSOManager.loginWeb targetUrl after handleTargetUrl', targetUrl);
        window.location.assign(targetUrl);
    }

    /**
     * Trigger the logout flow
     *
     * @returns {void}
     */
    logout = () => {
        console.debug('SSOManager.logout');
        if (!this.config?.logoutUrl) {
            this.logoutError('No SSOManager.config.logoutUrl provided');
            return;
        }
        if (this.isWeb) {
            this.logoutWeb();
        } else {
            this.logoutApp();
        }
    }

    /**
     * Handle logout flow for native apps
     *
     * @returns {void}
     */
    logoutApp = () => {
        console.debug('SSOManager.logoutApp');
        this.appAuthentication(this.config.logoutUrl);
    }

    /**
     * Handle logout flow for web platform
     * Redirects browser to SSO provider logout URL
     *
     * @returns {void}
     */
    logoutWeb = () => {
        console.debug('SSOManager.logoutWeb');
        const url = new URL(this.config.logoutUrl);
        url.searchParams.append(this.config?.urlParamKeys?.returnUrl, window.location.href);
        const targetUrl = url.toString();
        console.debug('SSOManager.logoutWeb targetUrl', targetUrl);
        window.location.assign(targetUrl);
    }

    /**
     * Complete the login/logout flow based on external state parameters
     * Validates URL parameters against config and determines if login or logout is required
     *
     * type SanitizedUrlParams = {
     *   token?: string;
     *   login?: string;
     *   purchase?: string;
     *   logout?: string;
     * }
     * type FinishLogInOut = async (SanitizedUrlParams) => Promise<void>
     */
    finishLogInOut = async ({ token, login, logout, purchase }) => {
        // check if the url param values matches the related configuration
        const externalState = {
            ...(token ? { token: this.checkExternalState('token', token) } : {}),
            ...(login ? { login: this.checkExternalState('login', login) } : {}),
            ...(purchase ? { purchase: this.checkExternalState('purchase', purchase) } : {}),
            ...(logout ? { logout: this.checkExternalState('logout', logout) } : {}),
        }

        console.debug('SSOManager.finishLogInOut externalState', externalState);

        // we got everything we needed from the url
        this.utils.clearUrlParams();

        // check if (re-)login is required
        if (await this.loginRequired(externalState)) {
            console.debug('SSOManager.finishLogInOut loginRequired');
            void this.finishLogin({ token });
        }
        // check if logout is required
        else if (await this.logoutRequired(externalState)) {
            console.debug('SSOManager.finishLogInOut logoutRequired');
            this.finishLogout();
        }
    }

    /**
     * Complete the login process
     * Logs out existing session if needed, then logs in with external token
     *
     * type FinishLogin = async ({ token?: string | undefined | null  }) => Promise<void>
     */
    finishLogin = async ({ token }) => {
        // logout before logging in since we cannot "re-login"
        const userData = await purple.entitlement.getUserData();
        if (userData.accessToken) {
            await purple.entitlement.logout();
        }

        let state;
        if (this.isWeb) {
            // in apps irrelevant
            state = {
                redirectUrl: this.utils.getStrippedUrl().toString(),
            }
        }

        purple.entitlement
            .login({
                externalToken: token,
                ...(state ? { state } : {})
            })
            .then(() => {
                if (this.isWeb) {
                    void this._onLogin.web();
                } else {
                    void this._onLogin.app();
                }
            })
            .catch(error => this.loginError(error));
    }

    /**
     * Complete the logout process
     * Calls purple.entitlement.logout and triggers onLogout hooks
     *
     * type FinishLogout = () => void
     */
    finishLogout = () => {
        purple.entitlement
            .logout()
            .then(() => {
                if (this.isWeb) {
                    void this._onLogout.web();
                } else {
                    void this._onLogout.app();
                }
            })
            .catch(error => this.logoutError(error));
    }

    /**
     * Handle login error
     *
     * type LoginError = (error: Error) => void
     */
    loginError = (error) => {
        console.error('login failed', error);
    }

    /**
     * Handle logout error
     *
     * type LogoutError = (error: Error) => void
     */
    logoutError = (error) => {
        console.error('logout failed', error);
    }

    /**
     * Finish login by:
     * - executing methods that might be different by project/environment
     * - reload the whole page so everything can properly update
     *
     * type OnLogin = {
     *   app: () => Promise<void>;
     *   web: () => Promise<void>;
     * }
     */
    _onLogin = {
        app: async () => {
            if (this.onLogin?.app) {
                await this.onLogin.app();
            }
            this.utils.reload();
        },
        web: async () => {
            if (this.onLogin?.web) {
                await this.onLogin.web();
            }
            this.utils.reload();
        }
    }

    /**
     * Finish logout by:
     * - executing methods that might be different by project/environment
     * - reload the whole page so everything can properly update
     *
     * type OnLogout = {
     *   app: () => Promise<void>;
     *   web: () => Promise<void>;
     * }
     */
    _onLogout = {
        app: async () => {
            if (this.onLogout?.app) {
                await this.onLogout.app();
            }
            this.utils.reload();
        },
        web: async () => {
            if (this.onLogout?.web) {
                await this.onLogout.web();
            }
            this.utils.reload();
        }
    }

    /**
     * Get and reset the custom return URL
     * We want to reset the _customReturnUrl variable to undefined as soon as we use it
     * so it cannot linger and pollute later login attempts
     *
     * type CustomReturnUrl = string | undefined
     * @returns {string | undefined} The custom return URL if set, undefined otherwise
     */
    get customReturnUrl() {
        const temp = this._customReturnUrl;
        this._customReturnUrl = undefined;
        return temp;
    }

    /**
     * Provides utility functions for URL handling and application state management
     */
    utils = {
        /**
         * Handle target URL by adding configured params and optionally the return URL
         *
         * type HandleTargetUrl = (targetUrl: string, withReturnUrl?: boolean) => string
         */
        handleTargetUrl: (targetUrl, withReturnUrl = true) => {
            console.debug('SSOManager.utils.handleTargetUrl targetUrl', targetUrl);

            const url = new URL(targetUrl);

            // add additional params defined in config to targetUrl
            Object.entries(this.config.targetUrlParams ?? {}).forEach(([key, value]) => {
                url.searchParams.append(key, value);
            })

            // adds return url as param to the targetUrl using the urlParamKeys.returnUrl from config
            if (withReturnUrl) {
                // the return url MUST be added as LAST param
                // otherwise later params might considered to belong to the return url
                const returnUrl = this.utils.getReturnUrl();
                url.searchParams.append(this.config?.urlParamKeys?.returnUrl, returnUrl);
            }
            console.debug('SSOManager.utils.handleTargetUrl returned url', url.toString());
            return url.toString();
        },
        /**
         * Reload application by either reloading or replacing current location with a different one.
         * Necessary because window.location.replace(newLocation) does not work when current and new location are identical.
         *
         * type Reload = () => void
         */
        reload: () => {
            const url = this.utils.getStrippedUrl();
            const newLocation = url.toString();
            console.debug('SSOManager.utils.reload currentLocation', window.location.href);
            console.debug('SSOManager.utils.reload newLocation', newLocation);
            if (window.location.href === newLocation) {
                window.location.reload();
            } else {
                window.location.replace(newLocation);
            }
        },
        /**
         * Create the URL we want to return to when coming back from SSO provider.
         * Only works for web (app is handled internally).
         * Uses customReturnUrl if set.
         * Adds additional parameters from config.returnUrlParams if present.
         *
         * type GetReturnUrl = () => string
         */
        getReturnUrl: () => {
            const returnUrl = new URL(this.customReturnUrl ?? window.location.href);
            Object.entries(this.config.returnUrlParams ?? {}).forEach(([key, value]) => {
                returnUrl.searchParams.append(key, value);
            })
            console.debug('SSOManager.utils.getReturnUrl returnUrl.toString()', returnUrl.toString());
            return returnUrl.toString();
        },
        /**
         * Create a URL from the current URL without login/logout relevant URL params
         * or manually declared ones in config.removeUrlParams
         *
         * type GetStrippedUrl = () => URL
         */
        getStrippedUrl: () => {
            const { token, login, logout, purchase } = this.config?.externalState ?? {};
            console.debug('SSOManager.utils.getStrippedUrl { token, login, logout, purchase }', { token, login, logout, purchase });
            const url = new URL(window.location.href);
            [
                ...(this.config?.removeUrlParams ?? []),
                `${this.config?.urlParamKeys?.loginViaUrl}`,
                `${this.config?.urlParamKeys?.checkoutViaUrl}`,
                `${this.config?.urlParamKeys?.logoutViaUrl}`,
                token[0],
                login[0],
                logout[0],
                purchase[0],
            ].forEach(param => { url.searchParams.delete(param) });
            return url;
        },
        /**
         * Replace the current URL with a clean one without any data regarding login/logout.
         * Uses getStrippedUrl() to create the clean URL.
         * Does NOT reload the application.
         *
         * type ClearUrlParams = () => void
         */
        clearUrlParams: () => {
            const url = this.utils.getStrippedUrl();
            window.history.replaceState(null, '', url.toString());
        },
        /**
         * Remap URL parameters to avoid always working with the keys we defined in config.externalState.
         * Converts external parameter names to normalized internal keys (token, login, purchase, logout).
         *
         * type ParamsObject = Record<string, string | undefined | null>
         * type NormalizedParamKey = 'token' | 'login' | 'purchase' | 'logout'
         * type SanitizedUrlParams = {
         *   token?: string;
         *   login?: string;
         *   purchase?: string;
         *   logout?: string;
         * }
         * type SanitizeUrlParams = (paramsObject: ParamsObject) => SanitizedUrlParams
         */
        sanitizeUrlParams: (paramsObject) => {
            console.debug('SSOManager.utils.sanitizeUrlParams paramsObject', paramsObject);
            if (!paramsObject) return {};

            const sanitizeUrlParam = (key) => {
                const urlParamKey = this.config?.externalState?.[key]?.[0];
                if (paramsObject.hasOwnProperty(urlParamKey)) {
                    return { [key]: paramsObject[urlParamKey] }
                }
                return {};
            }

            return {
                ...sanitizeUrlParam('token'),
                ...sanitizeUrlParam('login'),
                ...sanitizeUrlParam('purchase'),
                ...sanitizeUrlParam('logout'),
            }
        }
    }
};

```

init.js

```javascript
import { SSOManager } from './sso-manager';
import { extendStorefrontHook } from "../utils/extend-storefront-hook";

/**
 * Extended SSOManager class with project-specific login/logout hooks
 * @extends SSOManager
 */
class ExtendedSSOManager extends SSOManager {
    /**
     * Hooks to execute project-specific logic after successful login
     * @type {{ app: () => Promise<void>, web: () => Promise<void> }}
     */
    onLogin = {
        app: async () => {
            // do some project/environment specific stuff right after login in app
        },
        web: async () => {
            // do some project/environment specific stuff right after login in web
        }
    }

    /**
     * Hooks to execute project-specific logic after successful logout
     * @type {{ app: () => Promise<void>, web: () => Promise<void> }}
     */
    onLogout = {
        app: async () => {
            // do some project/environment specific stuff right after logout in app
        },
        web: async () => {
            // do some project/environment specific stuff right after logout in web
        }
    }
}

/**
 * Global singleton instance of ExtendedSSOManager
 * @type {ExtendedSSOManager | null}
 */
export let ssoManager = null;

/**
 * Initialize SSO Manager automatically when Purple Service is ready
 * Creates the singleton instance of ExtendedSSOManager on Purple Service initialization
 * This ensures SSO functionality is available as soon as the application is ready
 */
extendStorefrontHook('onPurpleServiceInit', () => {
    if (!ssoManager) {
        ssoManager = new ExtendedSSOManager();
    }
});

```
:::

