All files / src/components fingerprint-provider.tsx

100% Statements 31/31
100% Branches 22/22
100% Functions 11/11
100% Lines 29/29

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125                                                                    41x                       2x       34x 1x     33x                   41x 34x   34x 34x   34x   34x         34x     41x   41x 36x 1x     35x   35x     41x   41x   36x   35x         35x         41x 41x         41x     41x 25x       41x    
import { PropsWithChildren, useCallback, useEffect, useMemo, useRef } from 'react'
import { FingerprintContext } from '../fingerprint-context'
import { Agent, GetOptions, start, StartOptions, default as Loader } from '@fingerprint/agent'
import * as packageInfo from '../../package.json'
import { isSSR } from '../ssr'
import { WithEnvironment } from './with-environment'
import type { EnvDetails } from '../env.types'
import { usePromiseStore } from '../utils/use-promise-store'
 
export interface FingerprintProviderOptions extends StartOptions {
  /**
   * If set to `true`, will force the agent to be rebuilt with the new options. Should be used with caution
   * since it can be triggered too often (e.g. on every render) and negatively affect performance of the JS agent.
   */
  forceRebuild?: boolean
}
 
/**
 * @example
 * ```jsx
 * <FingerprintProvider
 *   apiKey="<your-fpjs-public-api-key>"
 * >
 *   <MyApp />
 * </FingerprintProvider>
 * ```
 *
 * Provides the FpContext to its child components.
 *
 * @privateRemarks
 * This is just a wrapper around the actual provider.
 * For the implementation, see `ProviderWithEnv` component.
 */
export function FingerprintProvider(props: PropsWithChildren<FingerprintProviderOptions>) {
  return <WithEnvironment>{(env) => <ProviderWithEnv {...props} env={env} />}</WithEnvironment>
}
 
interface ProviderWithEnvProps extends FingerprintProviderOptions {
  /**
   * Contains details about the env we're currently running in (e.g. framework, version)
   */
  env: EnvDetails
  getOptions?: GetOptions
}
 
function isLoader(value: unknown): value is Pick<typeof Loader, 'start'> {
  return typeof value === 'object' && value !== null && 'start' in value && typeof value.start === 'function'
}
 
function getCustomLoader(props: Record<string, unknown>) {
  if ('customAgent' in props && isLoader(props.customAgent)) {
    return props.customAgent
  }
 
  return undefined
}
 
function ProviderWithEnv({
  children,
  forceRebuild,
  env,
  getOptions,
  ...agentOptions
}: PropsWithChildren<ProviderWithEnvProps>) {
  const createClient = useCallback(() => {
    const customLoader = getCustomLoader(agentOptions)
 
    const envInfo = env.version !== undefined && env.version !== '' ? `${env.name}/${env.version}` : env.name
    const integrationInfo = `react-sdk/${packageInfo.version}/${envInfo}`
 
    const mergedIntegrationInfo = [...(agentOptions.integrationInfo ?? []), integrationInfo]
 
    const startParams = {
      ...agentOptions,
      integrationInfo: mergedIntegrationInfo,
    }
 
    return customLoader ? customLoader.start(startParams) : start(startParams)
  }, [agentOptions, env])
 
  const clientRef = useRef<Agent | undefined>(undefined)
 
  const getClient = useCallback(() => {
    if (isSSR()) {
      throw new Error('FingerprintProvider client cannot be used in SSR')
    }
 
    clientRef.current ??= createClient()
 
    return clientRef.current
  }, [createClient])
 
  const { doRequest } = usePromiseStore()
 
  const getVisitorData = useCallback(
    (options?: GetOptions) => {
      const client = getClient()
 
      const mergedOptions = {
        ...getOptions,
        ...options,
      }
 
      return doRequest(async () => client.get(mergedOptions), mergedOptions)
    },
    [doRequest, getClient, getOptions]
  )
 
  const contextValue = useMemo(() => {
    return {
      getVisitorData,
    }
  }, [getVisitorData])
 
  useEffect(() => {
    // By default, the client is always initialized once during the first render and won't be updated
    // if the configuration changes. Use `forceRebuild` flag to disable this behaviour.
    if (!clientRef.current || forceRebuild === true) {
      clientRef.current = createClient()
    }
  }, [forceRebuild, agentOptions, getOptions, createClient])
 
  return <FingerprintContext.Provider value={contextValue}>{children}</FingerprintContext.Provider>
}