All files / src/components fingerprint-provider.tsx

91.17% Statements 31/34
58.33% Branches 14/24
90% Functions 9/10
90.9% Lines 30/33

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 126 127 128 129 130 131 132 133 134 135 136 137 138                                                                    15x   15x                                       13x       13x                   15x 13x   13x   13x 13x   13x     13x   13x         13x     15x   15x 13x       13x 4x     13x     15x   15x   13x   13x         13x         15x 15x         15x     15x 9x       15x    
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>) {
  const propsWithEnv = props as PropsWithChildren<ProviderWithEnvProps>
 
  return (
    <WithEnvironment>
      <ProviderWithEnv {...propsWithEnv} />
    </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>) {
  Iif ('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)
 
    let integrationInfo = `react-sdk/${packageInfo.version}`
 
    Eif (env) {
      const envInfo = env.version ? `${env.name}/${env.version}` : env.name
 
      integrationInfo += `/${envInfo}`
    }
 
    const mergedIntegrationInfo = [...(agentOptions.integrationInfo || []), integrationInfo]
 
    const startParams = {
      ...agentOptions,
      integrationInfo: mergedIntegrationInfo,
    }
 
    return customLoader ? customLoader.start(startParams) : start(startParams)
  }, [agentOptions, env])
 
  const clientRef = useRef<Agent>()
 
  const getClient = useCallback(() => {
    Iif (isSSR()) {
      throw new Error('FingerprintProvider client cannot be used in SSR')
    }
 
    if (!clientRef.current) {
      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) {
      clientRef.current = createClient()
    }
  }, [forceRebuild, agentOptions, getOptions, createClient])
 
  return <FingerprintContext.Provider value={contextValue}>{children}</FingerprintContext.Provider>
}