@glydi/passkey-react 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 ADDED
@@ -0,0 +1,139 @@
1
+ # @glydi/passkey-react
2
+
3
+ Thin React/Next.js adapter for the Glide passkey Web Component.
4
+
5
+ ## Install
6
+
7
+ Glide is not yet published to npm. Install from a packed tarball or via `pnpm link`.
8
+
9
+ **Tarball (recommended):**
10
+
11
+ ```json
12
+ {
13
+ "dependencies": {
14
+ "@glydi/passkey-react": "file:../glide/dist-packs/glydi-passkey-react-0.1.0.tgz",
15
+ "@glydi/passkey-core": "file:../glide/dist-packs/glydi-passkey-core-0.1.0.tgz"
16
+ },
17
+ "pnpm": {
18
+ "overrides": {
19
+ "@glydi/passkey-core": "file:../glide/dist-packs/glydi-passkey-core-0.1.0.tgz"
20
+ }
21
+ }
22
+ }
23
+ ```
24
+
25
+ The `pnpm.overrides` entry for `@glydi/passkey-core` prevents pnpm from trying to fetch
26
+ the transitive peer from the npm registry. See [docs/DISTRIBUTION.md](../../docs/DISTRIBUTION.md)
27
+ for full tarball and `pnpm link` instructions, including how to produce the tarballs.
28
+
29
+ > **Forthcoming:** `npm install @glydi/passkey-react` will be the public form once the
30
+ > package is published. It is not yet available on npm.
31
+
32
+ ## Minimal Usage
33
+
34
+ ### Declarative `<PasskeyButton>`
35
+
36
+ `PasskeyButton` declares `"use client"` — it is a **Client Component** and must not be
37
+ imported from server-only code.
38
+
39
+ ```tsx
40
+ // Source: apps/example/app/page.tsx
41
+ "use client";
42
+ import { PasskeyButton } from "@glydi/passkey-react";
43
+
44
+ <PasskeyButton
45
+ mode="signup"
46
+ label="Register passkey"
47
+ onSuccess={(result) => console.log("registered", result.user.id)}
48
+ onError={(e) => console.warn(e.code, e.message)}
49
+ />
50
+ ```
51
+
52
+ ### Headless `usePasskeyAuth`
53
+
54
+ For custom UI, use the headless hook — it returns a `ref` to attach to a
55
+ `<biometric-auth-button>` element alongside reactive state:
56
+
57
+ ```tsx
58
+ // Source: packages/react/src/usePasskeyAuth.ts
59
+ "use client";
60
+ import { usePasskeyAuth } from "@glydi/passkey-react";
61
+ import "@glydi/passkey-core/define";
62
+
63
+ export default function MyPage() {
64
+ const { ref, phase, isPending, error, user } = usePasskeyAuth({
65
+ mode: "auto",
66
+ onSuccess: (r) => router.push("/app"),
67
+ });
68
+ return (
69
+ <biometric-auth-button ref={ref} label={isPending ? "…" : "Sign in"} />
70
+ );
71
+ }
72
+ ```
73
+
74
+ ## API
75
+
76
+ ### `PasskeyButton`
77
+
78
+ A React wrapper around `<biometric-auth-button>`. Declares **`"use client"`** — import
79
+ only in Client Components or pages marked `"use client"`.
80
+
81
+ **`PasskeyButtonProps`:**
82
+
83
+ | Prop | Type | Default | Description |
84
+ |------|------|---------|-------------|
85
+ | `mode` | `"auto" \| "signin" \| "signup"` | `"auto"` | Auth mode. See AuthMode callout below. |
86
+ | `username` | `string` | — | Username hint for registration/conditional UI. |
87
+ | `label` | `string` | — | Button label text. |
88
+ | `endpoints` | `Partial<GlideEndpoints>` | — | Override one or more API endpoint paths. |
89
+ | `fetchOptions` | `RequestInit` | — | Merged into every `fetch` call (e.g. custom headers). |
90
+ | `onSuccess` | `(result: AuthResult) => void` | — | Called after successful authentication. |
91
+ | `onError` | `(error: GlideError) => void` | — | Called when authentication fails. |
92
+ | `prime` | `boolean` | — | Show a pre-prompt explainer before the OS biometric sheet. |
93
+ | `primeTitle` | `string` | — | Title for the priming panel. |
94
+ | `primeBody` | `string` | — | Body text for the priming panel. |
95
+ | `primeContinue` | `string` | — | Continue button label in the priming panel. |
96
+ | `primeCancel` | `string` | — | Cancel button label in the priming panel. |
97
+ | `fallbackHref` | `string` | — | URL of a recovery page (e.g. magic-link). Shown on unsupported/error. |
98
+ | `fallbackLabel` | `string` | — | Label for the fallback link. |
99
+ | `className` | `string` | — | CSS class forwarded to the host element. |
100
+ | `style` | `React.CSSProperties` | — | Inline style forwarded to the host element. |
101
+
102
+ > **Valid `mode` values are `"auto"`, `"signin"`, and `"signup"` only.** Using `"register"`
103
+ > or `"authenticate"` is invalid and will silently no-op.
104
+
105
+ ### `usePasskeyAuth(options?)`
106
+
107
+ Headless hook. Returns a `UsePasskeyAuth` object:
108
+
109
+ | Return value | Type | Description |
110
+ |---|---|---|
111
+ | `ref` | `RefObject<GlideElement \| null>` | Attach to `<biometric-auth-button ref={ref}>`. |
112
+ | `phase` | `AuthPhase` | Current auth phase (`"idle"`, `"loading"`, `"success"`, etc.). |
113
+ | `isPending` | `boolean` | `true` during `"loading"` or `"authenticating"`. |
114
+ | `isUnsupported` | `boolean` | `true` when `phase === "unsupported"`. |
115
+ | `error` | `GlideError \| null` | Last error, or `null`. |
116
+ | `user` | `GlideUser \| null` | Authenticated user after success, or `null`. |
117
+ | `trigger()` | `() => void` | Imperatively start auth (same as clicking the button). |
118
+ | `reset()` | `() => void` | Reset phase and error back to `"idle"`. |
119
+
120
+ **`UsePasskeyAuthOptions`** (all optional):
121
+
122
+ | Option | Type | Description |
123
+ |--------|------|-------------|
124
+ | `mode` | `"auto" \| "signin" \| "signup"` | Auth mode. Default `"auto"`. |
125
+ | `username` | `string` | Username hint. |
126
+ | `endpoints` | `Partial<GlideEndpoints>` | Override API endpoint paths. |
127
+ | `fetchOptions` | `RequestInit` | Merged into every `fetch` call. |
128
+ | `onSuccess` | `(result: AuthResult) => void` | Success callback. |
129
+ | `onError` | `(error: GlideError) => void` | Error callback. |
130
+
131
+ ### Re-exported types
132
+
133
+ `AuthMode`, `AuthPhase`, `AuthResult`, `GlideEndpoints`, `GlideError`, `GlideUser`
134
+
135
+ ## Links
136
+
137
+ - [Root README](../../README.md) — architecture overview
138
+ - [Quickstart](../../docs/QUICKSTART.md) — full Next.js App Router integration walkthrough
139
+ - [Distribution guide](../../docs/DISTRIBUTION.md) — tarball and pnpm link install details
@@ -0,0 +1,105 @@
1
+ import { DetailedHTMLProps, HTMLAttributes } from 'react';
2
+ import { GlideEndpoints, AuthResult, GlideError, AuthPhase, GlideUser } from '@glydi/passkey-core';
3
+ export { AuthMode, AuthPhase, AuthResult, GlideEndpoints, GlideError, GlideUser } from '@glydi/passkey-core';
4
+
5
+ /**
6
+ * Ambient typing so TSX authors get autocomplete + type-checking on the
7
+ * underlying custom element, both for the modern `React.JSX` namespace (React 19)
8
+ * and the legacy global `JSX` namespace (React 18).
9
+ */
10
+
11
+ interface BiometricAuthButtonAttributes extends DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement> {
12
+ mode?: "auto" | "signin" | "signup";
13
+ username?: string;
14
+ label?: string;
15
+ "register-begin"?: string;
16
+ "register-finish"?: string;
17
+ "authenticate-begin"?: string;
18
+ "authenticate-finish"?: string;
19
+ prime?: boolean | "";
20
+ "prime-title"?: string;
21
+ "prime-body"?: string;
22
+ "prime-continue"?: string;
23
+ "prime-cancel"?: string;
24
+ "fallback-href"?: string;
25
+ "fallback-label"?: string;
26
+ }
27
+ declare global {
28
+ namespace JSX {
29
+ interface IntrinsicElements {
30
+ "biometric-auth-button": BiometricAuthButtonAttributes;
31
+ }
32
+ }
33
+ }
34
+
35
+ interface PasskeyButtonProps {
36
+ mode?: "auto" | "signin" | "signup";
37
+ username?: string;
38
+ label?: string;
39
+ endpoints?: Partial<GlideEndpoints>;
40
+ fetchOptions?: RequestInit;
41
+ onSuccess?: (result: AuthResult) => void;
42
+ onError?: (error: GlideError) => void;
43
+ /** Show the priming explainer before the native OS prompt. */
44
+ prime?: boolean;
45
+ primeTitle?: string;
46
+ primeBody?: string;
47
+ primeContinue?: string;
48
+ primeCancel?: string;
49
+ /** Recovery escape hatch (e.g. your magic-link page) shown on failure/unsupported. */
50
+ fallbackHref?: string;
51
+ fallbackLabel?: string;
52
+ /** Forwarded to the host element for ::part() theming hooks if needed. */
53
+ className?: string;
54
+ style?: React.CSSProperties;
55
+ }
56
+ /**
57
+ * Thin declarative wrapper. This is intentionally ~50 lines: all real work
58
+ * lives in the Web Component. The wrapper only:
59
+ * 1. ensures the element is registered (client-side import),
60
+ * 2. forwards object props the DOM can't express as attributes, and
61
+ * 3. bridges CustomEvents → React callbacks.
62
+ *
63
+ * For full state (phase/error/user) use `usePasskeyAuth` instead.
64
+ */
65
+ declare function PasskeyButton({ mode, username, label, endpoints, fetchOptions, onSuccess, onError, prime, primeTitle, primeBody, primeContinue, primeCancel, fallbackHref, fallbackLabel, className, style, }: PasskeyButtonProps): JSX.Element;
66
+
67
+ /** Element instance shape we rely on (subset of BiometricAuthButton). */
68
+ interface GlideElement extends HTMLElement {
69
+ phase: AuthPhase;
70
+ endpoints: Partial<GlideEndpoints>;
71
+ fetchOptions: RequestInit;
72
+ }
73
+ interface UsePasskeyAuthOptions {
74
+ mode?: "auto" | "signin" | "signup";
75
+ username?: string;
76
+ endpoints?: Partial<GlideEndpoints>;
77
+ /** Forwarded to fetch (e.g. custom headers). credentials:"include" is default. */
78
+ fetchOptions?: RequestInit;
79
+ onSuccess?: (result: AuthResult) => void;
80
+ onError?: (error: GlideError) => void;
81
+ }
82
+ interface UsePasskeyAuth {
83
+ /** Attach to <biometric-auth-button ref={ref} />. */
84
+ ref: React.RefObject<GlideElement | null>;
85
+ phase: AuthPhase;
86
+ /** True while the ceremony is in flight. */
87
+ isPending: boolean;
88
+ /** True once a feature-detect failure is reflected by the element. */
89
+ isUnsupported: boolean;
90
+ error: GlideError | null;
91
+ user: GlideUser | null;
92
+ /** Imperatively start auth (same as a user click). */
93
+ trigger: () => void;
94
+ reset: () => void;
95
+ }
96
+ /**
97
+ * Headless hook wrapping the <biometric-auth-button> Web Component.
98
+ *
99
+ * It owns nothing about rendering — you bring the element (via <PasskeyButton>
100
+ * or a raw tag) and spread `ref`. The hook subscribes to the element's
101
+ * `glide:*` CustomEvents and projects them into React state + callbacks.
102
+ */
103
+ declare function usePasskeyAuth(options?: UsePasskeyAuthOptions): UsePasskeyAuth;
104
+
105
+ export { PasskeyButton, type PasskeyButtonProps, type UsePasskeyAuth, type UsePasskeyAuthOptions, usePasskeyAuth };
package/dist/index.js ADDED
@@ -0,0 +1,137 @@
1
+ "use client";
2
+ import { useRef, useEffect, useState, useCallback, useMemo } from 'react';
3
+ import '@glydi/passkey-core/define';
4
+ import { jsx } from 'react/jsx-runtime';
5
+
6
+ // src/PasskeyButton.tsx
7
+ function PasskeyButton({
8
+ mode = "auto",
9
+ username,
10
+ label,
11
+ endpoints,
12
+ fetchOptions,
13
+ onSuccess,
14
+ onError,
15
+ prime,
16
+ primeTitle,
17
+ primeBody,
18
+ primeContinue,
19
+ primeCancel,
20
+ fallbackHref,
21
+ fallbackLabel,
22
+ className,
23
+ style
24
+ }) {
25
+ const ref = useRef(null);
26
+ useEffect(() => {
27
+ const el = ref.current;
28
+ if (!el)
29
+ return;
30
+ if (endpoints)
31
+ el.endpoints = endpoints;
32
+ if (fetchOptions)
33
+ el.fetchOptions = fetchOptions;
34
+ }, [endpoints, fetchOptions]);
35
+ const handlers = useRef({ onSuccess, onError });
36
+ handlers.current = { onSuccess, onError };
37
+ useEffect(() => {
38
+ const el = ref.current;
39
+ if (!el)
40
+ return;
41
+ const s = (e) => handlers.current.onSuccess?.(e.detail);
42
+ const f = (e) => handlers.current.onError?.(e.detail);
43
+ el.addEventListener("glide:success", s);
44
+ el.addEventListener("glide:error", f);
45
+ return () => {
46
+ el.removeEventListener("glide:success", s);
47
+ el.removeEventListener("glide:error", f);
48
+ };
49
+ }, []);
50
+ return /* @__PURE__ */ jsx(
51
+ "biometric-auth-button",
52
+ {
53
+ ref,
54
+ mode,
55
+ username,
56
+ label,
57
+ ...prime ? { prime: "" } : {},
58
+ ...primeTitle ? { "prime-title": primeTitle } : {},
59
+ ...primeBody ? { "prime-body": primeBody } : {},
60
+ ...primeContinue ? { "prime-continue": primeContinue } : {},
61
+ ...primeCancel ? { "prime-cancel": primeCancel } : {},
62
+ ...fallbackHref ? { "fallback-href": fallbackHref } : {},
63
+ ...fallbackLabel ? { "fallback-label": fallbackLabel } : {},
64
+ className,
65
+ style
66
+ }
67
+ );
68
+ }
69
+ function usePasskeyAuth(options = {}) {
70
+ const ref = useRef(null);
71
+ const [phase, setPhase] = useState("idle");
72
+ const [error, setError] = useState(null);
73
+ const [user, setUser] = useState(null);
74
+ const cbs = useRef(options);
75
+ cbs.current = options;
76
+ useEffect(() => {
77
+ const el = ref.current;
78
+ if (!el)
79
+ return;
80
+ if (options.endpoints)
81
+ el.endpoints = options.endpoints;
82
+ if (options.fetchOptions)
83
+ el.fetchOptions = options.fetchOptions;
84
+ }, [options.endpoints, options.fetchOptions]);
85
+ useEffect(() => {
86
+ const el = ref.current;
87
+ if (!el)
88
+ return;
89
+ const onSuccess = (e) => {
90
+ const detail = e.detail;
91
+ setUser(detail.user);
92
+ setError(null);
93
+ cbs.current.onSuccess?.(detail);
94
+ };
95
+ const onError = (e) => {
96
+ const detail = e.detail;
97
+ setError(detail);
98
+ cbs.current.onError?.(detail);
99
+ };
100
+ const onPhase = (e) => {
101
+ setPhase(e.detail.phase);
102
+ };
103
+ el.addEventListener("glide:success", onSuccess);
104
+ el.addEventListener("glide:error", onError);
105
+ el.addEventListener("glide:phasechange", onPhase);
106
+ setPhase(el.phase);
107
+ return () => {
108
+ el.removeEventListener("glide:success", onSuccess);
109
+ el.removeEventListener("glide:error", onError);
110
+ el.removeEventListener("glide:phasechange", onPhase);
111
+ };
112
+ }, []);
113
+ const trigger = useCallback(() => {
114
+ ref.current?.click();
115
+ }, []);
116
+ const reset = useCallback(() => {
117
+ setError(null);
118
+ setUser(null);
119
+ }, []);
120
+ return useMemo(
121
+ () => ({
122
+ ref,
123
+ phase,
124
+ isPending: phase === "loading" || phase === "authenticating",
125
+ isUnsupported: phase === "unsupported",
126
+ error,
127
+ user,
128
+ trigger,
129
+ reset
130
+ }),
131
+ [phase, error, user, trigger, reset]
132
+ );
133
+ }
134
+
135
+ export { PasskeyButton, usePasskeyAuth };
136
+ //# sourceMappingURL=out.js.map
137
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/PasskeyButton.tsx","../src/usePasskeyAuth.ts"],"names":["useEffect","useRef"],"mappings":"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","sourcesContent":["\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport \"@glydi/passkey-core/define\"; // registers <biometric-auth-button> (client-only)\nimport type {\n AuthResult,\n GlideEndpoints,\n GlideError,\n} from \"@glydi/passkey-core\";\n\nexport interface PasskeyButtonProps {\n mode?: \"auto\" | \"signin\" | \"signup\";\n username?: string;\n label?: string;\n endpoints?: Partial<GlideEndpoints>;\n fetchOptions?: RequestInit;\n onSuccess?: (result: AuthResult) => void;\n onError?: (error: GlideError) => void;\n /** Show the priming explainer before the native OS prompt. */\n prime?: boolean;\n primeTitle?: string;\n primeBody?: string;\n primeContinue?: string;\n primeCancel?: string;\n /** Recovery escape hatch (e.g. your magic-link page) shown on failure/unsupported. */\n fallbackHref?: string;\n fallbackLabel?: string;\n /** Forwarded to the host element for ::part() theming hooks if needed. */\n className?: string;\n style?: React.CSSProperties;\n}\n\ninterface GlideElement extends HTMLElement {\n endpoints: Partial<GlideEndpoints>;\n fetchOptions: RequestInit;\n}\n\n/**\n * Thin declarative wrapper. This is intentionally ~50 lines: all real work\n * lives in the Web Component. The wrapper only:\n * 1. ensures the element is registered (client-side import),\n * 2. forwards object props the DOM can't express as attributes, and\n * 3. bridges CustomEvents → React callbacks.\n *\n * For full state (phase/error/user) use `usePasskeyAuth` instead.\n */\nexport function PasskeyButton({\n mode = \"auto\",\n username,\n label,\n endpoints,\n fetchOptions,\n onSuccess,\n onError,\n prime,\n primeTitle,\n primeBody,\n primeContinue,\n primeCancel,\n fallbackHref,\n fallbackLabel,\n className,\n style,\n}: PasskeyButtonProps) {\n const ref = useRef<GlideElement | null>(null);\n\n // Forward non-serializable props (objects) imperatively.\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n if (endpoints) el.endpoints = endpoints;\n if (fetchOptions) el.fetchOptions = fetchOptions;\n }, [endpoints, fetchOptions]);\n\n // Bridge events → callbacks. Callbacks kept in refs to avoid re-binding.\n const handlers = useRef({ onSuccess, onError });\n handlers.current = { onSuccess, onError };\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n const s = (e: Event) =>\n handlers.current.onSuccess?.((e as CustomEvent<AuthResult>).detail);\n const f = (e: Event) =>\n handlers.current.onError?.((e as CustomEvent<GlideError>).detail);\n el.addEventListener(\"glide:success\", s);\n el.addEventListener(\"glide:error\", f);\n return () => {\n el.removeEventListener(\"glide:success\", s);\n el.removeEventListener(\"glide:error\", f);\n };\n }, []);\n\n return (\n <biometric-auth-button\n ref={ref}\n mode={mode}\n username={username}\n label={label}\n {...(prime ? { prime: \"\" } : {})}\n {...(primeTitle ? { \"prime-title\": primeTitle } : {})}\n {...(primeBody ? { \"prime-body\": primeBody } : {})}\n {...(primeContinue ? { \"prime-continue\": primeContinue } : {})}\n {...(primeCancel ? { \"prime-cancel\": primeCancel } : {})}\n {...(fallbackHref ? { \"fallback-href\": fallbackHref } : {})}\n {...(fallbackLabel ? { \"fallback-label\": fallbackLabel } : {})}\n className={className}\n style={style}\n />\n );\n}\n","\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type {\n AuthPhase,\n AuthResult,\n GlideEndpoints,\n GlideError,\n GlideUser,\n} from \"@glydi/passkey-core\";\n\n/** Element instance shape we rely on (subset of BiometricAuthButton). */\ninterface GlideElement extends HTMLElement {\n phase: AuthPhase;\n endpoints: Partial<GlideEndpoints>;\n fetchOptions: RequestInit;\n}\n\nexport interface UsePasskeyAuthOptions {\n mode?: \"auto\" | \"signin\" | \"signup\";\n username?: string;\n endpoints?: Partial<GlideEndpoints>;\n /** Forwarded to fetch (e.g. custom headers). credentials:\"include\" is default. */\n fetchOptions?: RequestInit;\n onSuccess?: (result: AuthResult) => void;\n onError?: (error: GlideError) => void;\n}\n\nexport interface UsePasskeyAuth {\n /** Attach to <biometric-auth-button ref={ref} />. */\n ref: React.RefObject<GlideElement | null>;\n phase: AuthPhase;\n /** True while the ceremony is in flight. */\n isPending: boolean;\n /** True once a feature-detect failure is reflected by the element. */\n isUnsupported: boolean;\n error: GlideError | null;\n user: GlideUser | null;\n /** Imperatively start auth (same as a user click). */\n trigger: () => void;\n reset: () => void;\n}\n\n/**\n * Headless hook wrapping the <biometric-auth-button> Web Component.\n *\n * It owns nothing about rendering — you bring the element (via <PasskeyButton>\n * or a raw tag) and spread `ref`. The hook subscribes to the element's\n * `glide:*` CustomEvents and projects them into React state + callbacks.\n */\nexport function usePasskeyAuth(\n options: UsePasskeyAuthOptions = {},\n): UsePasskeyAuth {\n const ref = useRef<GlideElement | null>(null);\n const [phase, setPhase] = useState<AuthPhase>(\"idle\");\n const [error, setError] = useState<GlideError | null>(null);\n const [user, setUser] = useState<GlideUser | null>(null);\n\n // Keep callbacks fresh without re-subscribing listeners every render.\n const cbs = useRef(options);\n cbs.current = options;\n\n // Push imperative config (objects can't be set as attributes) onto the element.\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n if (options.endpoints) el.endpoints = options.endpoints;\n if (options.fetchOptions) el.fetchOptions = options.fetchOptions;\n }, [options.endpoints, options.fetchOptions]);\n\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n\n const onSuccess = (e: Event) => {\n const detail = (e as CustomEvent<AuthResult>).detail;\n setUser(detail.user);\n setError(null);\n cbs.current.onSuccess?.(detail);\n };\n const onError = (e: Event) => {\n const detail = (e as CustomEvent<GlideError>).detail;\n setError(detail);\n cbs.current.onError?.(detail);\n };\n const onPhase = (e: Event) => {\n setPhase((e as CustomEvent<{ phase: AuthPhase }>).detail.phase);\n };\n\n el.addEventListener(\"glide:success\", onSuccess);\n el.addEventListener(\"glide:error\", onError);\n el.addEventListener(\"glide:phasechange\", onPhase);\n // Sync any phase already reflected before listeners attached.\n setPhase(el.phase);\n\n return () => {\n el.removeEventListener(\"glide:success\", onSuccess);\n el.removeEventListener(\"glide:error\", onError);\n el.removeEventListener(\"glide:phasechange\", onPhase);\n };\n }, []);\n\n const trigger = useCallback(() => {\n ref.current?.click();\n }, []);\n\n const reset = useCallback(() => {\n setError(null);\n setUser(null);\n }, []);\n\n return useMemo(\n () => ({\n ref,\n phase,\n isPending: phase === \"loading\" || phase === \"authenticating\",\n isUnsupported: phase === \"unsupported\",\n error,\n user,\n trigger,\n reset,\n }),\n [phase, error, user, trigger, reset],\n );\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@glydi/passkey-react",
3
+ "version": "0.1.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "Thin React/Next.js adapter for the Glide passkey Web Component.",
8
+ "type": "module",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "main": "./dist/index.js",
16
+ "module": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "peerDependencies": {
22
+ "react": ">=18",
23
+ "react-dom": ">=18"
24
+ },
25
+ "dependencies": {
26
+ "@glydi/passkey-core": "0.1.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/react": "^18.2.0",
30
+ "react": "^18.2.0",
31
+ "tsup": "^8.0.0",
32
+ "typescript": "^5.4.0"
33
+ },
34
+ "license": "MIT",
35
+ "scripts": {
36
+ "build": "tsup",
37
+ "dev": "tsup --watch",
38
+ "typecheck": "tsc --noEmit",
39
+ "clean": "rm -rf dist .turbo"
40
+ }
41
+ }