Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ To identify visitors, you need a Fingerprint Pro account (you can [sign up for f

Configure the SDK by wrapping your application in `FingerprintProvider`.

> ⚠️ **Important**: Applications should create only one client across the entire app, either through `FingerprintProvider` or through the API client (`start()`). If you create another client, it will overwrite the underlying native client of the first one.

```javascript
// src/index.js
import React from 'react';
Expand Down Expand Up @@ -249,6 +251,8 @@ export default function App() {

Create a client with `start()` and call `get()`:

> ⚠️ **Important**: Applications should create only one client across the entire app, either through `FingerprintProvider` or through the API client (`start()`). If you create another client, it will overwrite the underlying native client of the first one.

```javascript
import React, { useEffect } from 'react';
import { start } from '@fingerprint/react-native';
Expand Down Expand Up @@ -284,7 +288,7 @@ The response is a flat, snake_case object that matches the Fingerprint Server AP

```typescript
interface FingerprintResponse {
visitor_id: string
visitor_id?: string // `undefined` if [zero-trust-mode](https://docs.fingerprint.com/docs/zero-trust-mode) is enabled
event_id: string
suspect_score?: number // present only when Smart Signals are enabled
sealed_result: string | null // base64 sealed result, or null when unavailable
Expand Down
6 changes: 5 additions & 1 deletion sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ To identify visitors, you need a Fingerprint Pro account (you can [sign up for f

Configure the SDK by wrapping your application in `FingerprintProvider`.

> ⚠️ **Important**: Applications should create only one client across the entire app, either through `FingerprintProvider` or through the API client (`start()`). If you create another client, it will overwrite the underlying native client of the first one.

```javascript
// src/index.js
import React from 'react';
Expand Down Expand Up @@ -249,6 +251,8 @@ export default function App() {

Create a client with `start()` and call `get()`:

> ⚠️ **Important**: Applications should create only one client across the entire app, either through `FingerprintProvider` or through the API client (`start()`). If you create another client, it will overwrite the underlying native client of the first one.

```javascript
import React, { useEffect } from 'react';
import { start } from '@fingerprint/react-native';
Expand Down Expand Up @@ -284,7 +288,7 @@ The response is a flat, snake_case object that matches the Fingerprint Server AP

```typescript
interface FingerprintResponse {
visitor_id: string
visitor_id?: string // `undefined` if [zero-trust-mode](https://docs.fingerprint.com/docs/zero-trust-mode) is enabled
event_id: string
suspect_score?: number // present only when Smart Signals are enabled
sealed_result: string | null // base64 sealed result, or null when unavailable
Expand Down
19 changes: 19 additions & 0 deletions sdk/__tests__/tags.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { validateTags } from '../src/tags'

describe('validateTags', () => {
it.each([Infinity, -Infinity, NaN])('rejects invalid numeric values', (value) => {
expect(() => {
validateTags(value)
}).toThrow(new TypeError('tags must be a finite number'))
})

it.each([Infinity, -Infinity, NaN])('rejects nested invalid numeric values', (value) => {
expect(() => {
validateTags({
value: {
numeric: value,
},
})
}).toThrow(new TypeError("tags['value']['numeric'] must be a finite number"))
})
})
2 changes: 1 addition & 1 deletion sdk/android/fingerprint.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ext.fingerprint_native_sdk_version = "[4.0.0, 5.0.0)"
ext.fingerprint_native_sdk_version = "[4.1.0, 5.0.0)"

task printFingerprintNativeSDKVersion {
doLast {
Expand Down
4 changes: 2 additions & 2 deletions sdk/ios/FPError+React.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ extension APIError {
// The SDK's `APIError.Code` has no explicit raw values, so `rawValue` is the camelCase case name
// (e.g. `tooManyRequests`). Map it to the canonical snake_case codes shared with Android and
// `@fingerprint/agent` so `FingerprintError.code` is identical across platforms. Falls back to
// the generic `failed` code when the server error carries no recognized code.
// the generic `unknown_error` code when the server error carries no recognized code.
var reactCode: String {
guard let code = self.errorDetails?.code else {
return "failed"
return "unknown_error"
}
switch code {
case .requestCannotBeParsed: return "request_cannot_be_parsed"
Expand Down
15 changes: 9 additions & 6 deletions sdk/src/FingerprintClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import RNFingerprint, { type NativeVisitorData } from './specs/NativeRNFingerpri
import type { FingerprintClient, FingerprintResponse, GetOptions, StartOptions, TagsValue } from './types'
import { unwrapError } from './unwrapError'
import { isDefined, isTruthy } from './utils'
import { validateTags } from './tags'

const packageVersion = '__VERSION__'

Expand Down Expand Up @@ -37,7 +38,7 @@ function toNativeTag(tag: TagsValue | undefined): CodegenTypes.UnsafeObject | nu

function normalizeResponse(data: NativeVisitorData): FingerprintResponse {
return {
visitor_id: data.visitorId,
visitor_id: isTruthy(data.visitorId) ? data.visitorId : undefined,
event_id: data.eventId,
suspect_score: data.suspectScore >= 0 ? data.suspectScore : undefined,
sealed_result: isTruthy(data.sealedResult) ? data.sealedResult : null,
Expand Down Expand Up @@ -65,12 +66,10 @@ class NativeFingerprintClient implements FingerprintClient {
}

public async get(options?: GetOptions): Promise<FingerprintResponse> {
validateTags(options?.tags)
const nativeTags = toNativeTag(options?.tags)
try {
const data = await RNFingerprint.getVisitorData(
toNativeTag(options?.tags),
options?.linkedId ?? null,
options?.timeout ?? null
)
const data = await RNFingerprint.getVisitorData(nativeTags, options?.linkedId ?? null, options?.timeout ?? null)
return normalizeResponse(data)
} catch (error) {
throw unwrapError(error)
Expand All @@ -81,6 +80,10 @@ class NativeFingerprintClient implements FingerprintClient {
/**
* Creates a Fingerprint client with the given options.
*
* Applications should create only one client: either through the API client (`start()`)
* or through a single `FingerprintProvider`. Creating another client will overwrite the underlying native client of the
* previous one.
*
* @group API Client approach
*
* @example
Expand Down
2 changes: 1 addition & 1 deletion sdk/src/FingerprintClient.web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,17 @@
}

public async get(options?: GetOptions): Promise<FingerprintResponse> {
try {
const result = await this.agent.get({
tags: options?.tags,
linkedId: options?.linkedId,
timeout: options?.timeout,
})

return {
// `visitor_id` is only omitted by the backend in zeroTrust mode; default to '' to keep the
// cross-platform shape stable.
visitor_id: result.visitor_id ?? '',
visitor_id: result.visitor_id ?? undefined,

Check warning on line 39 in sdk/src/FingerprintClient.web.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

Check warning on line 39 in sdk/src/FingerprintClient.web.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
event_id: result.event_id,
suspect_score: result.suspect_score,
sealed_result: result.sealed_result ? result.sealed_result.base64() : null,
Expand Down
4 changes: 4 additions & 0 deletions sdk/src/FingerprintProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { usePromiseStore } from './usePromiseStore'
/**
* Provides the {@link FingerprintContext} to its child components.
*
* Applications should create only one client: either through a single `FingerprintProvider`
* at the root of the component tree or through the API client (`start()`). Creating another
* client will overwrite the underlying native client of the previous one.
*
* @group Hooks approach
*
* @example
Expand Down
2 changes: 2 additions & 0 deletions sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ export * from './useFingerprint'

export { FingerprintError, isFingerprintError, FingerprintContextNotAvailableError, ErrorCode } from './errors'

export { validateTags } from './tags'

export * from './types'
52 changes: 52 additions & 0 deletions sdk/src/tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Rejects NaN / Infinity in `tags`.
*
* `TagsPrimitive` is `number`, so `{ score: Number('oops') }` type-checks.
* JSON has no literal for those values. Each platform rewrites them
* differently (`null` on web, `0` on iOS via `NSNumber.intValue`, usually
* `null` on Android) and identification still succeeds. Throw instead of
* storing the wrong tag.
*
* `Date`, `Map`, and class instances are already a type error on `TagsValue`.
* This walk does not reject them. The RN bridge and JS agent do not agree on
* those values (a `Date` is an ISO string on web and `{}` on native), but we
* choose to ignore that.
*
* Size is a server `payload_too_large`.
* https://docs.fingerprint.com/docs/tagging-information
*/

export function validateTags(tags?: unknown): void {
if (tags === null || tags === undefined) {
return
}
walk(tags, 'tags')
}

function walk(value: unknown, path: string): void {
if (value === null || value === undefined) {
return
}

Check warning on line 29 in sdk/src/tags.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

if (typeof value === 'number') {
if (!Number.isFinite(value)) {
throw new TypeError(`${path} must be a finite number`)
}
return
}

if (typeof value !== 'object') {
return
}

if (Array.isArray(value)) {
for (const [index, entry] of value.entries()) {
walk(entry, `${path}[${String(index)}]`)
}
return
}

for (const [key, entry] of Object.entries(value)) {
walk(entry, `${path}['${key}']`)
}
}
4 changes: 2 additions & 2 deletions sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,9 @@ export interface GetOptions {
*/
export interface FingerprintResponse {
/**
* The visitor identifier.
* The visitor identifier. `undefined` if [zero-trust-mode](https://docs.fingerprint.com/docs/zero-trust-mode) is enabled.
*/
visitor_id: string
visitor_id?: string
Comment thread
ilfa marked this conversation as resolved.
/**
* A unique id associated with the successful identification request.
*/
Expand Down
Loading