@magield/react 0.0.0-stage → 0.7.1

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/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2026 MagicShield. All rights reserved.
2
+
3
+ MAGIC SHIELD SDK LICENCE
4
+
5
+ This package is published to a public registry so that licensed customers can
6
+ install it with ordinary tooling. Being able to download it does not grant any
7
+ right to use it.
8
+
9
+ You may install, use and redistribute this package, unmodified and only as part
10
+ of your own application, solely to integrate that application with a Magic
11
+ Shield deployment that you are licensed to use under a written agreement with
12
+ MagicShield ("your Agreement"), and only for the term of your Agreement.
13
+
14
+ Except as your Agreement expressly allows, you may not:
15
+
16
+ - use this package for any other purpose, including to build, benchmark or
17
+ assist a competing product;
18
+ - modify it, or reverse engineer, decompile, deobfuscate or disassemble it,
19
+ except to the extent this restriction is unenforceable under applicable law;
20
+ - remove or alter this notice or any copyright or attribution in it.
21
+
22
+ THE PACKAGE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXCEPT AS YOUR
23
+ AGREEMENT PROVIDES. IN NO EVENT SHALL MAGICSHIELD BE LIABLE FOR ANY CLAIM,
24
+ DAMAGES OR OTHER LIABILITY ARISING FROM THE PACKAGE OR ITS USE, EXCEPT AS YOUR
25
+ AGREEMENT PROVIDES.
26
+
27
+ Your Agreement governs if it conflicts with this notice.
@@ -0,0 +1,11 @@
1
+ import { type Shield, type ShieldConfig } from '@magield/web';
2
+ export declare const ShieldContext: import("react").Context<Shield | undefined>;
3
+ /**
4
+ * One Shield instance per provider.
5
+ *
6
+ * Instance identity matters: warmed signals and the resolved device id live on
7
+ * it, so recreating it on every render would discard the work that makes
8
+ * `collect()` fast and re-resolve storage each time.
9
+ */
10
+ export declare function createInstance(config: ShieldConfig): Shield;
11
+ export declare function useShieldInstance(): Shield;
@@ -0,0 +1,20 @@
1
+ import { createShield } from '@magield/web';
2
+ import { createContext, useContext } from 'react';
3
+ export const ShieldContext = createContext(undefined);
4
+ /**
5
+ * One Shield instance per provider.
6
+ *
7
+ * Instance identity matters: warmed signals and the resolved device id live on
8
+ * it, so recreating it on every render would discard the work that makes
9
+ * `collect()` fast and re-resolve storage each time.
10
+ */
11
+ export function createInstance(config) {
12
+ return createShield(config);
13
+ }
14
+ export function useShieldInstance() {
15
+ const shield = useContext(ShieldContext);
16
+ if (shield === undefined) {
17
+ throw new Error('useShield must be used inside <ShieldProvider>. Wrap your app once, at the root.');
18
+ }
19
+ return shield;
20
+ }
@@ -0,0 +1,4 @@
1
+ export { ShieldProvider, type ShieldProviderProps } from './provider.js';
2
+ export { useShield, useShieldToken, type UseShieldResult } from './use-shield.js';
3
+ export { ShieldContext } from './context.js';
4
+ export type { CollectOptions, CollectResult, ShieldConfig } from '@magield/web';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { ShieldProvider } from './provider.js';
2
+ export { useShield, useShieldToken } from './use-shield.js';
3
+ export { ShieldContext } from './context.js';
@@ -0,0 +1,13 @@
1
+ import type { ShieldConfig } from '@magield/web';
2
+ import { type ReactNode } from 'react';
3
+ export interface ShieldProviderProps {
4
+ readonly config: ShieldConfig;
5
+ /**
6
+ * Collect the expensive signals on mount, so that by the time a user submits
7
+ * a form the work is already done. On by default: it is the difference
8
+ * between collection costing nothing and costing 60ms at the worst moment.
9
+ */
10
+ readonly warmup?: boolean;
11
+ readonly children: ReactNode;
12
+ }
13
+ export declare function ShieldProvider({ config, warmup, children }: ShieldProviderProps): ReactNode;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useMemo } from 'react';
3
+ import { createInstance, ShieldContext } from './context.js';
4
+ export function ShieldProvider({ config, warmup = true, children }) {
5
+ // Keyed on the site key rather than the config object, because an inline
6
+ // object literal is a new reference on every render and would otherwise
7
+ // rebuild the instance continuously.
8
+ const shield = useMemo(() => createInstance(config), [config.siteKey]);
9
+ useEffect(() => {
10
+ if (!warmup)
11
+ return;
12
+ // Deliberately not awaited and never surfaced: warmup is an optimisation,
13
+ // and a failed one must not reach the integrator's error boundary.
14
+ void shield.warmup();
15
+ }, [shield, warmup]);
16
+ return _jsx(ShieldContext.Provider, { value: shield, children: children });
17
+ }
@@ -0,0 +1,25 @@
1
+ import type { CollectOptions, CollectResult } from '@magield/web';
2
+ export interface UseShieldResult {
3
+ /**
4
+ * Collects and returns a token. Nothing is sent anywhere — where the token
5
+ * travels is your decision.
6
+ */
7
+ collect(options: CollectOptions): Promise<CollectResult>;
8
+ /** True once warmup has finished. Collection works before this; it is slower. */
9
+ readonly ready: boolean;
10
+ readonly deviceId: string | null;
11
+ /** True while a collect() is in flight, for disabling a submit button. */
12
+ readonly collecting: boolean;
13
+ }
14
+ export declare function useShield(): UseShieldResult;
15
+ /**
16
+ * Collects once for a fixed action and keeps the result.
17
+ *
18
+ * For pages where the action is known ahead of time and the token is wanted
19
+ * before the user acts. Tokens are short-lived and single-use, so this
20
+ * re-collects when the action changes rather than caching indefinitely.
21
+ */
22
+ export declare function useShieldToken(action: string): {
23
+ token: string | null;
24
+ refresh: () => Promise<void>;
25
+ };
@@ -0,0 +1,61 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { useShieldInstance } from './context.js';
3
+ export function useShield() {
4
+ const shield = useShieldInstance();
5
+ const [ready, setReady] = useState(false);
6
+ const [deviceId, setDeviceId] = useState(null);
7
+ const [collecting, setCollecting] = useState(false);
8
+ const mounted = useRef(true);
9
+ useEffect(() => {
10
+ mounted.current = true;
11
+ return () => {
12
+ mounted.current = false;
13
+ };
14
+ }, []);
15
+ useEffect(() => {
16
+ let cancelled = false;
17
+ void (async () => {
18
+ await shield.warmup();
19
+ const id = await shield.getDeviceId();
20
+ // Guarded, because a form can be unmounted while warmup is in flight and
21
+ // setting state afterwards is a warning the integrator would see, not us.
22
+ if (!cancelled && mounted.current) {
23
+ setDeviceId(id);
24
+ setReady(true);
25
+ }
26
+ })();
27
+ return () => {
28
+ cancelled = true;
29
+ };
30
+ }, [shield]);
31
+ const collect = useCallback(async (options) => {
32
+ setCollecting(true);
33
+ try {
34
+ return await shield.collect(options);
35
+ }
36
+ finally {
37
+ if (mounted.current)
38
+ setCollecting(false);
39
+ }
40
+ }, [shield]);
41
+ return { collect, ready, deviceId, collecting };
42
+ }
43
+ /**
44
+ * Collects once for a fixed action and keeps the result.
45
+ *
46
+ * For pages where the action is known ahead of time and the token is wanted
47
+ * before the user acts. Tokens are short-lived and single-use, so this
48
+ * re-collects when the action changes rather than caching indefinitely.
49
+ */
50
+ export function useShieldToken(action) {
51
+ const { collect } = useShield();
52
+ const [token, setToken] = useState(null);
53
+ const refresh = useCallback(async () => {
54
+ const result = await collect({ action });
55
+ setToken(result.token === '' ? null : result.token);
56
+ }, [collect, action]);
57
+ useEffect(() => {
58
+ void refresh();
59
+ }, [refresh]);
60
+ return { token, refresh };
61
+ }
package/package.json CHANGED
@@ -1,6 +1,45 @@
1
1
  {
2
2
  "name": "@magield/react",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.7.1",
4
+ "description": "React bindings for the Magic Shield web SDK",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "files": [
7
+ "dist",
8
+ "!**/*.map",
9
+ "LICENSE"
10
+ ],
11
+ "publishConfig": {
12
+ "access": "public",
13
+ "registry": "https://registry.npmjs.org/"
14
+ },
15
+ "type": "module",
16
+ "exports": {
17
+ ".": "./dist/index.js"
18
+ },
19
+ "main": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "sideEffects": false,
22
+ "peerDependencies": {
23
+ "react": ">=18"
24
+ },
25
+ "dependencies": {
26
+ "@magield/web": "0.7.1"
27
+ },
28
+ "devDependencies": {
29
+ "@testing-library/react": "^16.1.0",
30
+ "@types/node": "^22.10.2",
31
+ "@types/react": "^19.0.2",
32
+ "happy-dom": "^15.11.7",
33
+ "react": "^19.0.0",
34
+ "react-dom": "^19.0.0",
35
+ "typescript": "^5.7.2",
36
+ "vitest": "^2.1.8"
37
+ },
38
+ "scripts": {
39
+ "build": "tsc -p tsconfig.json",
40
+ "typecheck": "tsc -p tsconfig.json --noEmit",
41
+ "typecheck:tests": "tsc -p tsconfig.test.json",
42
+ "test": "vitest run",
43
+ "clean": "rm -rf dist .turbo"
44
+ }
6
45
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.