@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 +139 -0
- package/dist/index.d.ts +105 -0
- package/dist/index.js +137 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|