@fingerly/react wraps the JavaScript SDK in a provider and hooks. However many components ask, a page makes one identification and every component reads the same answer.
Requirements
- React 19 or newer.
- A public key with your site in its allowed origins.
Install
npm install @fingerly/react
Add the provider
import { createRoot } from 'react-dom/client'
import { FingerlyProvider } from '@fingerly/react'
createRoot(document.getElementById('root')!).render(
<FingerlyProvider apiKey="fly_pk_us_production_…">
<App />
</FingerlyProvider>,
)
The provider collects nothing when it mounts. It accepts apiKey, endpoint, endpoints, fallbackToDefaultEndpoint, submit and consent, with the meanings the JavaScript agent gives them. Changing consent takes effect without remounting, and setConsent() is also on the store from useFingerly().
Identify on an action
import { useIdentify } from '@fingerly/react'
export function Checkout({ orderId }: { orderId: string }) {
const { identify, isLoading, error } = useIdentify({ tag: () => 'checkout:' + orderId })
async function onSubmit() {
const { requestId } = await identify()
await submitOrder({ orderId, requestId })
}
return (
<>
<button onClick={onSubmit} disabled={isLoading}>
{isLoading ? 'Checking…' : 'Pay now'}
</button>
{error && <p>{error.message}</p>}
</>
)
}
Identify on mount
const { suspectLevel, isReady } = useIdentify({ immediate: true })
immediate runs in an effect, so it never runs during server rendering. suspend: true suspends the component instead, for use inside <Suspense>.
useIdentify
Options
immediatebooleanDefaultfalseIdentify when the component mounts.suspendbooleanDefaultfalseSuspend during render until the identification finishes.tagstring | () => string | undefinedThe tag to send. A function is read at the moment of submission. Atagpassed toidentify()overrides it.
Returns
requestIdstring | nullIdentifies this identification. Send it to your server with the action it protects.visitorIdstring | nullThe stable identifier the server resolved for this browser.visitorIsNewbooleanWhether your organization is seeing this visitor for the first time.visitorConfidencenumberHow sure the identification is, from 0 to 100.identifiablebooleanfalsewhen the browser gave too little to identify anyone.duplicatebooleantruewhen the server had already answered this request.statestring | nullenriched, orunavailablewhen the network lookup could not run.suspectScorenumber | nullThe server's weighted score.nullwhen nothing was scored, which is not the same as0.suspectLevelstring | nulllow,mediumorhigh.triggersIdentifyTrigger[]The signals the server scored, heaviest first.verdictsVerdicts | nullThe local, advisory client-side verdicts.errorError | nullThe last failure, if the identification failed.isLoadingbooleanAn identification is in flight.isReadybooleanThe identification has finished, successfully or not.isSuspiciousbooleanAny local verdict matched atmediumconfidence or above.identify(options?: { tag?: string; force?: boolean }) => Promise<IdentifyResult>Identify, or return the page's existing identification.forcecollects afresh.refresh(options?: { tag?: string }) => Promise<IdentifyResult>Identify again, ignoring the shared result.
identify and refresh are stable across renders. A failed identification is forgotten, so the next call tries again.
useVerdict
Reads one local verdict, with a confidence floor. It never triggers identification itself.
import { useVerdict } from '@fingerly/react'
function AutomationNotice() {
const { matched, confidence } = useVerdict('automation', { min: 'high' })
if (!matched) return null
return <aside>Automated access suspected ({confidence})</aside>
}
Returns matched, verdict, confidence and reasons. min defaults to 'medium'. Verdict names are incognito, shields, tor, emulator, automation and farm.
Server rendering
- On the server the hooks return the unidentified state, so hydration always matches.
- Calling
identify()during server rendering rejects withFingerlyServerError. Call it from an effect or an event handler. - For a server that renders many requests, create a store per request with
createFingerlyStore(options)and pass it as<FingerlyProvider store={store}>.
Errors
Failures are stored in error and rethrown from identify(). TransportError is re-exported with its status and retryable fields. Using a hook outside a provider throws an error that says where to add one.