@fingerprintchecker/react 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -2
- package/dist/index.d.ts +14 -0
- package/dist/index.js +103 -0
- package/package.json +31 -4
package/README.md
CHANGED
|
@@ -1,3 +1,89 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @fingerprintchecker/react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React provider and hook for [FingerPrintChecker](https://fpchecker.com). Built on `@fingerprintchecker/agent`. Requires React 18 or newer.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @fingerprintchecker/react
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { FpCheckerProvider, useFpChecker } from '@fingerprintchecker/react';
|
|
11
|
+
|
|
12
|
+
<FpCheckerProvider publishableKey="pk_live_...">
|
|
13
|
+
<App />
|
|
14
|
+
</FpCheckerProvider>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The provider accepts the same options as `load()` from `@fingerprintchecker/agent`: `publishableKey`, optional `endpoint`, optional `scriptUrl`. The agent loads in the browser after mount; nothing loads during server rendering.
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
const { identify, data, error, isLoading } = useFpChecker();
|
|
21
|
+
const result = await identify({ intentToken, linkedId, cache: { ttlSeconds: 60, storage: 'session' } });
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- `identify(opts)` passes through to the agent, updates `data`, `error`, `isLoading`, returns the result and rethrows on failure.
|
|
25
|
+
- If several calls overlap, the most recently started call decides `data`.
|
|
26
|
+
- A failure to load the agent shows up as `error`.
|
|
27
|
+
- `useFpChecker()` outside the provider throws an error naming `FpCheckerProvider`.
|
|
28
|
+
|
|
29
|
+
## Next.js App Router
|
|
30
|
+
|
|
31
|
+
The package files start with `'use client'`, so they can be imported from client components.
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
// app/fp-provider.tsx
|
|
35
|
+
'use client';
|
|
36
|
+
import { FpCheckerProvider } from '@fingerprintchecker/react';
|
|
37
|
+
|
|
38
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
39
|
+
return <FpCheckerProvider publishableKey={process.env.NEXT_PUBLIC_FPCHECKER_KEY!}>{children}</FpCheckerProvider>;
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
// app/layout.tsx (server component)
|
|
45
|
+
import { Providers } from './fp-provider';
|
|
46
|
+
|
|
47
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
48
|
+
return (
|
|
49
|
+
<html lang="en">
|
|
50
|
+
<body>
|
|
51
|
+
<Providers>{children}</Providers>
|
|
52
|
+
</body>
|
|
53
|
+
</html>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
// app/signup/signup-button.tsx
|
|
60
|
+
'use client';
|
|
61
|
+
import { useFpChecker } from '@fingerprintchecker/react';
|
|
62
|
+
|
|
63
|
+
export function SignupButton({ intentToken }: { intentToken: string }) {
|
|
64
|
+
const { identify, isLoading, error } = useFpChecker();
|
|
65
|
+
|
|
66
|
+
async function onClick() {
|
|
67
|
+
const result = await identify({ intentToken });
|
|
68
|
+
// Send the result to YOUR backend, which verifies it with the server SDK.
|
|
69
|
+
await fetch('/api/signup', {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: { 'content-type': 'application/json' },
|
|
72
|
+
body: JSON.stringify({ eventId: result.eventId, sealedResult: result.sealedResult }),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<>
|
|
78
|
+
<button onClick={onClick} disabled={isLoading}>Sign up</button>
|
|
79
|
+
{error && <p role="alert">{error.message}</p>}
|
|
80
|
+
</>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Call `identify` from a handler, not during render.
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
Apache-2.0
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IdentifyOptions, IdentifyResult, LoadOptions } from '@fingerprintchecker/agent';
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
3
|
+
export type { IdentifyOptions, IdentifyResult, LoadOptions } from '@fingerprintchecker/agent';
|
|
4
|
+
export interface FpCheckerState {
|
|
5
|
+
identify(opts: IdentifyOptions): Promise<IdentifyResult>;
|
|
6
|
+
data?: IdentifyResult;
|
|
7
|
+
error?: Error;
|
|
8
|
+
isLoading: boolean;
|
|
9
|
+
}
|
|
10
|
+
export type FpCheckerProviderProps = LoadOptions & {
|
|
11
|
+
children?: ReactNode;
|
|
12
|
+
};
|
|
13
|
+
export declare function FpCheckerProvider(props: FpCheckerProviderProps): import("react").JSX.Element;
|
|
14
|
+
export declare function useFpChecker(): FpCheckerState;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { FpCheckerAgentError, load } from '@fingerprintchecker/agent';
|
|
4
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
|
5
|
+
const FpCheckerContext = createContext(null);
|
|
6
|
+
function toError(e) {
|
|
7
|
+
return e instanceof Error ? e : new Error(String(e));
|
|
8
|
+
}
|
|
9
|
+
export function FpCheckerProvider(props) {
|
|
10
|
+
const { publishableKey, endpoint, scriptUrl, children } = props;
|
|
11
|
+
const [data, setData] = useState(undefined);
|
|
12
|
+
const [error, setError] = useState(undefined);
|
|
13
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
14
|
+
const mounted = useRef(true);
|
|
15
|
+
const callId = useRef(0);
|
|
16
|
+
const agentRef = useRef(null);
|
|
17
|
+
const prevKey = useRef(null);
|
|
18
|
+
const optionsKey = JSON.stringify([publishableKey, endpoint ?? null, scriptUrl ?? null]);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
mounted.current = true;
|
|
21
|
+
return () => {
|
|
22
|
+
mounted.current = false;
|
|
23
|
+
};
|
|
24
|
+
}, []);
|
|
25
|
+
const getAgent = useCallback(() => {
|
|
26
|
+
if (agentRef.current?.key !== optionsKey) {
|
|
27
|
+
const opts = {
|
|
28
|
+
publishableKey,
|
|
29
|
+
...(endpoint !== undefined ? { endpoint } : {}),
|
|
30
|
+
...(scriptUrl !== undefined ? { scriptUrl } : {}),
|
|
31
|
+
};
|
|
32
|
+
const promise = load(opts);
|
|
33
|
+
agentRef.current = { key: optionsKey, promise };
|
|
34
|
+
// Do not cache a failed load: the next identify retries.
|
|
35
|
+
promise.catch(() => {
|
|
36
|
+
if (agentRef.current?.promise === promise)
|
|
37
|
+
agentRef.current = null;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return agentRef.current.promise;
|
|
41
|
+
}, [optionsKey, publishableKey, endpoint, scriptUrl]);
|
|
42
|
+
// Client only: effects never run during server rendering.
|
|
43
|
+
// A change of options starts a new generation: old calls become stale and state resets.
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
let active = true;
|
|
46
|
+
// Only a real change resets: on first mount (and the StrictMode re-run) children's
|
|
47
|
+
// mount-time identify calls have already started and must stay current.
|
|
48
|
+
if (prevKey.current !== null && prevKey.current !== optionsKey) {
|
|
49
|
+
callId.current += 1;
|
|
50
|
+
setData(undefined);
|
|
51
|
+
setError(undefined);
|
|
52
|
+
setIsLoading(false);
|
|
53
|
+
}
|
|
54
|
+
prevKey.current = optionsKey;
|
|
55
|
+
getAgent().catch((e) => {
|
|
56
|
+
if (active)
|
|
57
|
+
setError(toError(e));
|
|
58
|
+
});
|
|
59
|
+
return () => {
|
|
60
|
+
active = false;
|
|
61
|
+
};
|
|
62
|
+
}, [getAgent, optionsKey]);
|
|
63
|
+
const identify = useCallback(async (opts) => {
|
|
64
|
+
if (typeof window === 'undefined') {
|
|
65
|
+
throw new FpCheckerAgentError('ssr', 'identify() can only run in the browser; call it from an event handler or effect.');
|
|
66
|
+
}
|
|
67
|
+
const id = ++callId.current;
|
|
68
|
+
const isLatest = () => mounted.current && callId.current === id;
|
|
69
|
+
setIsLoading(true);
|
|
70
|
+
try {
|
|
71
|
+
const agent = await getAgent();
|
|
72
|
+
const result = await agent.identify(opts);
|
|
73
|
+
if (isLatest()) {
|
|
74
|
+
setData(result);
|
|
75
|
+
setError(undefined);
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
if (isLatest())
|
|
81
|
+
setError(toError(e));
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
if (isLatest())
|
|
86
|
+
setIsLoading(false);
|
|
87
|
+
}
|
|
88
|
+
}, [getAgent]);
|
|
89
|
+
const value = useMemo(() => ({
|
|
90
|
+
identify,
|
|
91
|
+
isLoading,
|
|
92
|
+
...(data !== undefined ? { data } : {}),
|
|
93
|
+
...(error !== undefined ? { error } : {}),
|
|
94
|
+
}), [identify, data, error, isLoading]);
|
|
95
|
+
return _jsx(FpCheckerContext.Provider, { value: value, children: children });
|
|
96
|
+
}
|
|
97
|
+
export function useFpChecker() {
|
|
98
|
+
const ctx = useContext(FpCheckerContext);
|
|
99
|
+
if (!ctx) {
|
|
100
|
+
throw new Error('useFpChecker must be used inside <FpCheckerProvider>. Wrap your app (or layout) with it.');
|
|
101
|
+
}
|
|
102
|
+
return ctx;
|
|
103
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fingerprintchecker/react",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React provider and hook for FingerPrintChecker.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": ["dist", "README.md"],
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "rm -rf dist && tsc -p tsconfig.json",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"prepack": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": { "@fingerprintchecker/agent": "^0.1.0" },
|
|
22
|
+
"peerDependencies": { "react": ">=18" },
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@testing-library/react": "^16.0.0",
|
|
25
|
+
"@types/react": "^18.3.0",
|
|
26
|
+
"@types/react-dom": "^18.3.0",
|
|
27
|
+
"happy-dom": "^15.11.0",
|
|
28
|
+
"react": "^18.3.1",
|
|
29
|
+
"react-dom": "^18.3.1",
|
|
30
|
+
"typescript": "^5.5.0",
|
|
31
|
+
"vitest": "^2.1.0"
|
|
32
|
+
}
|
|
33
|
+
}
|