Source

CryptrProvider.tsx

import React, { useEffect, useReducer, useState, useCallback, useRef } from 'react'
import Client from '../../node_modules/@cryptr/cryptr-spa-js/dist/types/client'
import CryptrContext from './CryptrContext'
import initialCryptrState from './initialCryptrState'
import CryptrReducer from './CryptrReducer'
import { CryptrTokenClaims, ProviderConfig, User } from './utils/cryptr.interfaces'
import { Config, SsoSignOptsAttrs } from '@cryptr/cryptr-spa-js/dist/types/interfaces'
import CryptrSpa from '@cryptr/cryptr-spa-js'

/**
 * The default action to perform after authentication:
 *
 * *Removes query params from history state*
 * @category Defaults
 * */
const DEFAULT_REDIRECT_CALLBACK = () => {
  try {
    window.history.replaceState({}, document.title, window.location.pathname)
  } catch (error) {
    console.error(error)
  }
}

/**
 * The default action after successful logout:
 *
 * *Popup alert informing user is logged out + reload the page*
 * @category Defaults
 */
const DEFAULT_LOGOUT_CALLBACK = () => {
  alert('you are logged out')
  window.location.reload()
}

const DEFAULT_SCOPE = 'email profile openid'

interface ProviderOptions extends Config {
  onRedirectCallback?: (claims: CryptrTokenClaims | null) => void
  onLogOutCallback?: () => void
  defaultScopes?: string
}
interface ProviderProps extends ProviderOptions {
  children: JSX.Element
}

const prepareConfig = (options: ProviderOptions): ProviderConfig => {
  return {
    ...options,
    tenant_domain: options.tenant_domain,
    client_id: options.client_id,
    audience: options.audience || window.location.origin,
    cryptr_base_url: options.cryptr_base_url,
    default_redirect_uri: options.default_redirect_uri || window.location.origin,
    onRedirectCallback: options.onRedirectCallback || DEFAULT_REDIRECT_CALLBACK,
    onLogOutCallback: options.onLogOutCallback || DEFAULT_LOGOUT_CALLBACK,
    defaultScopes: options.defaultScopes || DEFAULT_SCOPE,
    dedicated_server: options.dedicated_server || false,
    default_slo_after_revoke: options.default_slo_after_revoke ?? false,
  }
}

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, react/prop-types
const CryptrProvider = (props: ProviderProps): JSX.Element => {
  const { children, ...options } = props
  const [config] = useState<ProviderConfig>(prepareConfig(options))

  const [cryptrClient] = useState<Client>(new CryptrSpa.client(config))
  const [state, dispatch] = useReducer(CryptrReducer, initialCryptrState)

  const logOutCallback = () => {
    try {
      dispatchNewState({ type: 'INITIALIZED', isAuthenticated: false, user: null })
    } catch (error) {
      console.error('logoutCallback error')
      console.error(error)
    }
  }

  const popupHandler = useCallback(
    () => {
      cryptrClient.logOut(logOutCallback)
    },
    // eslint-disable-next-line
    [cryptrClient],
  )

  const dispatchNewState = (newState) => {
    dispatch(newState)
  }

  useEffect(() => {
    const configFn = async () => {
      try {
        if (cryptrClient && cryptrClient.canHandleAuthentication()) {
          const tokens = await cryptrClient.handleRedirectCallback()
          const claims = cryptrClient.getClaimsFromAccess(
            tokens.accessToken,
          ) as unknown as CryptrTokenClaims | null
          config.onRedirectCallback(claims)
        } else if (cryptrClient && cryptrClient.canRefresh(cryptrClient.getRefreshStore())) {
          await cryptrClient.handleRefreshTokens()
        } else {
          console.log('not hanling redirection')
        }
      } catch (error) {
        console.error('catched error', error)
        if (error instanceof Error) {
          dispatchNewState({ type: 'ERROR', error: error.message })
        } else {
          dispatchNewState({ type: 'ERROR', error: error })
        }
      } finally {
        if (cryptrClient !== undefined) {
          const user = cryptrClient.getUser() as unknown as User | null
          const isAuthenticated = await cryptrClient.isAuthenticated()
          dispatchNewState({ type: 'INITIALIZED', isAuthenticated, user })
        }
      }
    }
    configFn()
  }, [config, cryptrClient])

  const useEventListener = (eventName: string, handler, element = window) => {
    const savedHandler = useRef(handler)

    useEffect(() => {
      savedHandler.current = handler
    })

    useEffect(() => {
      const isSupported = element && element.addEventListener
      if (!isSupported) return

      const eventListener = (event) => {
        if (savedHandler !== undefined) {
          savedHandler.current(event)
        }
      }
      element.addEventListener(eventName, eventListener)
      return () => {
        element.removeEventListener(eventName, eventListener)
      }
    }, [eventName, element])
  }

  useEventListener(CryptrSpa.events.REFRESH_EXPIRED, popupHandler)
  useEventListener(CryptrSpa.events.REFRESH_INVALID_GRANT, popupHandler)

  if (cryptrClient === undefined) {
    return children
  }

  return (
    <CryptrContext.Provider
      data-testid="CryptrProvider"
      value={{
        ...state,
        isAuthenticated: () => state.isAuthenticated,
        logOut: async (callback?: () => void, targetUrl?: string, sloAfterRevoke?: boolean) =>
          cryptrClient.logOut(
            callback || logOutCallback,
            undefined,
            targetUrl,
            sloAfterRevoke || config.default_slo_after_revoke,
          ),
        signInWithEmail: (email: string, options?: SsoSignOptsAttrs) =>
          cryptrClient.signInWithEmail(email, options),
        signInWithDomain: (organizationDomain: string, options?: SsoSignOptsAttrs) =>
          cryptrClient.signInWithDomain(organizationDomain, options),
        user: () => state.user,
        decoratedRequest: (url: string, kyOptions?: object | undefined) => {
          return cryptrClient.decoratedRequest(url, kyOptions)
        },
        config: () => config,
        defaultScopes: () => config.defaultScopes,
        getCurrentAccessToken: () => cryptrClient.getCurrentAccessToken(),
        getCurrentIdToken: () => cryptrClient.getCurrentIdToken(),
      }}
    >
      {children}
    </CryptrContext.Provider>
  )
}

export default CryptrProvider