All files / src use-visitor-data.ts

100% Statements 51/51
100% Branches 21/21
100% Functions 10/10
100% Lines 50/50

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 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175                                                                                      81x   81x   81x 81x 81x             81x 27x               81x 29x               81x 5x 5x           5x     81x   22x   21x 21x   21x         21x 18x   18x   3x                         81x 38x 22x     16x   16x 16x 16x 13x 11x     3x 1x     2x 2x       16x   16x 6x           81x   81x   81x 2x     81x 6x     81x 6x 75x   1x     81x 76x              
import { FingerprintContext, FingerprintContextInterface, VisitorQueryResult } from './fingerprint-context'
import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { toError } from './utils/to-error'
import { assertIsDefined } from './utils/assert-is-defined'
import { areGetOptionsEqual } from './utils/get-options-cache-key'
import { GetOptions, GetResult } from '@fingerprint/agent'
 
export interface UseVisitorDataConfig {
  /**
   * Controls automatic visitor data fetching. When `true`, the hook fetches after mounting, and whenever the
   * request options change. When changed from `false` to `true`, visitor data is initiated after the current render.
   */
  immediate: boolean
}
 
export type UseVisitorDataOptions = GetOptions & UseVisitorDataConfig
 
export type UseVisitorDataReturn = VisitorQueryResult & {
  /**
   * Performs identification request to server and returns visitors data.
   * */
  getData: (getDataOptions?: GetOptions) => Promise<GetResult>
}
 
/**
 *  @example
 * ```js
 *  const {
 *    // Request state
 *    data,
 *    isLoading,
 *    error,
 *    // A method to be called manually when the `immediate` field in the config is set to `false`:
 *    getData,
 *  } = useVisitorData({ immediate: false });
 * ```
 * Use the `useVisitorData` hook in your components to perform identification requests with the Fingerprint API. The returned object contains information about loading status, errors, and visitor.
 *
 * @param {UseVisitorDataOptions} options for the `agent.get()` request and for hook
 */
export function useVisitorData(
  { immediate, ...getOptions }: UseVisitorDataOptions = { immediate: true }
): UseVisitorDataReturn {
  assertIsDefined(getOptions, 'getOptions')
 
  const { getVisitorData } = useContext<FingerprintContextInterface>(FingerprintContext)
 
  const [currentGetOptions, setCurrentGetOptions] = useState(getOptions)
  const [currentImmediate, setCurrentImmediate] = useState(immediate)
  const [queryState, setQueryState] = useState<VisitorQueryResult>({
    isLoading: immediate,
    data: undefined,
    isFetched: false,
    error: undefined,
  })
 
  const setLoading = () => {
    setQueryState({
      isLoading: true,
      isFetched: false,
      data: undefined,
      error: undefined,
    })
  }
 
  const setSuccess = (data: GetResult) => {
    setQueryState({
      isLoading: false,
      isFetched: true,
      data,
      error: undefined,
    })
  }
 
  const setFailure = (unknownError: unknown) => {
    const error = toError(unknownError)
    setQueryState({
      isLoading: false,
      isFetched: false,
      data: undefined,
      error,
    })
    return error
  }
 
  const getData = useCallback<UseVisitorDataReturn['getData']>(
    async (params = {}) => {
      assertIsDefined(params, 'getDataParams')
 
      try {
        setLoading()
 
        const getDataOptions: GetOptions = {
          ...currentGetOptions,
          ...params,
        }
 
        const result = await getVisitorData(getDataOptions)
        setSuccess(result)
 
        return result
      } catch (unknownError) {
        throw setFailure(unknownError)
      }
    },
    [currentGetOptions, getVisitorData]
  )
 
  /**
   * When `immediate` is enabled, fetches visitor data on mount and whenever `getOptions` change.
   * We don't reuse `getData` here because it sets loading state synchronously, which is not allowed
   * inside an effect — `isLoading: true` is covered by the initial state and the render-phase reset below.
   * The `ignore` flag prevents an outdated in-flight response from overwriting a newer request's state:
   * https://react.dev/reference/react/useEffect#fetching-data-with-effects
   */
  useEffect(() => {
    if (!currentImmediate) {
      return
    }
 
    let ignore = false
 
    const fetchVisitorData = async () => {
      try {
        const result = await getVisitorData(currentGetOptions)
        if (!ignore) {
          setSuccess(result)
        }
      } catch (unknownError: unknown) {
        if (ignore) {
          return
        }
 
        const error = setFailure(unknownError)
        console.error(`Failed to fetch visitor data automatically: ${error}`)
      }
    }
 
    void fetchVisitorData()
 
    return () => {
      ignore = true
    }
  }, [currentImmediate, getVisitorData, currentGetOptions])
 
  // When automatic-fetch inputs change, store them and reset to loading during render so the effect can start
  // the request without synchronously setting state: https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes
  const didImmediateChange = currentImmediate !== immediate
  const didGetOptionsChange =
    !Object.is(currentGetOptions, getOptions) && !areGetOptionsEqual(currentGetOptions, getOptions)
 
  if (didImmediateChange) {
    setCurrentImmediate(immediate)
  }
 
  if (didGetOptionsChange) {
    setCurrentGetOptions(getOptions)
  }
 
  if (immediate && (didImmediateChange || didGetOptionsChange)) {
    setLoading()
  } else if (didImmediateChange) {
    // Disabling automatic fetching clears loading while the pending response is ignored.
    setQueryState((state) => ({ ...state, isLoading: false }))
  }
 
  return useMemo(
    () => ({
      ...queryState,
      getData,
    }),
    [queryState, getData]
  )
}