@touchque/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/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +37 -0
- package/dist/index.d.mts +66 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +114 -0
- package/dist/index.mjs +100 -0
- package/package.json +68 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file. The
|
|
4
|
+
format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
|
+
|
|
6
|
+
## [0.1.0] — 2026-08-27
|
|
7
|
+
|
|
8
|
+
Initial release.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `useTouchQuePasskey(config)` — a hook over `@touchque/web` that memoizes one
|
|
12
|
+
client and tracks `status` (`idle` | `pending` | `success` | `stepup` |
|
|
13
|
+
`pending2fa` | `error`), `error` and `result`. Exposes `authenticate`,
|
|
14
|
+
`register`, `list`, `remove`, `reset` and `isSupported`.
|
|
15
|
+
- `<PasskeyButton config email onSuccess onStepUp onPending2fa onError />` —
|
|
16
|
+
a button that runs the passwordless-primary flow and routes the outcome to
|
|
17
|
+
callbacks; `disabled` + `data-loading` while the ceremony runs;
|
|
18
|
+
`hideWhenUnsupported` to render nothing when WebAuthn is unavailable.
|
|
19
|
+
- Re-exports `PasskeyDismissedError` / `PasskeyNotRegisteredError` /
|
|
20
|
+
`PasskeyDisabledError` from `@touchque/web`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TouchQue
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @touchque/react
|
|
2
|
+
|
|
3
|
+
React bindings for [`@touchque/web`](https://www.npmjs.com/package/@touchque/web) —
|
|
4
|
+
a passkey sign-in hook and a drop-in button.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install @touchque/react @touchque/web react
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
import { PasskeyButton } from '@touchque/react';
|
|
12
|
+
|
|
13
|
+
const config = { baseUrl: 'https://api.example.com' }; // your relay backend
|
|
14
|
+
|
|
15
|
+
<PasskeyButton
|
|
16
|
+
config={config}
|
|
17
|
+
email={email}
|
|
18
|
+
onSuccess={(r) => (window.location.href = r.raw.redirect_url as string)}
|
|
19
|
+
onStepUp={() => startPushFlow()}
|
|
20
|
+
onError={(e) => setError(e.message)}
|
|
21
|
+
/>;
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or drive it yourself with the hook:
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { useTouchQuePasskey } from '@touchque/react';
|
|
28
|
+
|
|
29
|
+
const pk = useTouchQuePasskey(config);
|
|
30
|
+
// pk.status: 'idle' | 'pending' | 'success' | 'stepup' | 'pending2fa' | 'error'
|
|
31
|
+
|
|
32
|
+
<button disabled={!pk.isSupported || pk.status === 'pending'} onClick={() => pk.authenticate(email)}>
|
|
33
|
+
Sign in with a passkey
|
|
34
|
+
</button>;
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Full documentation is in the repo.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { AuthenticateResult, RegisterResult, PasskeySummary, TouchQueWebConfig } from '@touchque/web';
|
|
2
|
+
export { PasskeyDisabledError, PasskeyDismissedError, PasskeyNotRegisteredError } from '@touchque/web';
|
|
3
|
+
import * as react from 'react';
|
|
4
|
+
|
|
5
|
+
type PasskeyStatus = 'idle' | 'pending' | 'success' | 'stepup' | 'pending2fa' | 'error';
|
|
6
|
+
interface UseTouchQuePasskey {
|
|
7
|
+
/** `true` if this browser exposes the WebAuthn API. */
|
|
8
|
+
isSupported: boolean;
|
|
9
|
+
/** Current state of the last `authenticate()` call. */
|
|
10
|
+
status: PasskeyStatus;
|
|
11
|
+
/** The last error, or `null`. */
|
|
12
|
+
error: Error | null;
|
|
13
|
+
/** The last `authenticate()` result, or `null`. */
|
|
14
|
+
result: AuthenticateResult | null;
|
|
15
|
+
/** Passwordless-primary sign-in. Updates `status` / `error` / `result`. */
|
|
16
|
+
authenticate: (email: string, context?: Record<string, unknown>) => Promise<AuthenticateResult>;
|
|
17
|
+
/** Register a passkey for the signed-in user. */
|
|
18
|
+
register: (extra?: Record<string, unknown>) => Promise<RegisterResult>;
|
|
19
|
+
/** List the signed-in user's passkeys. */
|
|
20
|
+
list: () => Promise<PasskeySummary[]>;
|
|
21
|
+
/** Remove one of the signed-in user's passkeys. */
|
|
22
|
+
remove: (id: string) => Promise<{
|
|
23
|
+
deleted: boolean;
|
|
24
|
+
}>;
|
|
25
|
+
/** Reset `status` / `error` / `result` to their initial values. */
|
|
26
|
+
reset: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* React hook over `@touchque/web`. Memoizes one client for the given config
|
|
31
|
+
* and tracks the state of the passkey sign-in flow.
|
|
32
|
+
*
|
|
33
|
+
* ```tsx
|
|
34
|
+
* const pk = useTouchQuePasskey({ baseUrl: 'https://api.example.com' });
|
|
35
|
+
* <button disabled={!pk.isSupported} onClick={() => pk.authenticate(email)}>
|
|
36
|
+
* Sign in with a passkey
|
|
37
|
+
* </button>
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
declare function useTouchQuePasskey(config: TouchQueWebConfig): UseTouchQuePasskey;
|
|
41
|
+
|
|
42
|
+
interface PasskeyButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onError' | 'children'> {
|
|
43
|
+
/** `@touchque/web` config (`baseUrl`, `paths?`, `credentials?`, …). */
|
|
44
|
+
config: TouchQueWebConfig;
|
|
45
|
+
/** The account to sign in. */
|
|
46
|
+
email: string;
|
|
47
|
+
/** Called when the passkey assertion signed the user in. */
|
|
48
|
+
onSuccess?: (result: AuthenticateResult) => void;
|
|
49
|
+
/** Called when risk/policy wants a second factor (push / number match). */
|
|
50
|
+
onStepUp?: (result: AuthenticateResult) => void;
|
|
51
|
+
/** Called when the relay wants the caller to continue a pending 2FA flow. */
|
|
52
|
+
onPending2fa?: (result: AuthenticateResult) => void;
|
|
53
|
+
/** Called on any error (dismissed prompt, no passkey, disabled, network). */
|
|
54
|
+
onError?: (error: Error) => void;
|
|
55
|
+
/** Hide the button entirely when WebAuthn is unavailable. Default: false. */
|
|
56
|
+
hideWhenUnsupported?: boolean;
|
|
57
|
+
children?: React.ReactNode;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A button that runs the passwordless-primary passkey sign-in and routes the
|
|
61
|
+
* outcome to `onSuccess` / `onStepUp` / `onPending2fa` / `onError`. While the
|
|
62
|
+
* ceremony is running the button is `disabled` and `data-loading` is set.
|
|
63
|
+
*/
|
|
64
|
+
declare function PasskeyButton({ config, email, onSuccess, onStepUp, onPending2fa, onError, hideWhenUnsupported, children, disabled, onClick, ...buttonProps }: PasskeyButtonProps): react.JSX.Element | null;
|
|
65
|
+
|
|
66
|
+
export { PasskeyButton, type PasskeyButtonProps, type PasskeyStatus, type UseTouchQuePasskey, useTouchQuePasskey };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { AuthenticateResult, RegisterResult, PasskeySummary, TouchQueWebConfig } from '@touchque/web';
|
|
2
|
+
export { PasskeyDisabledError, PasskeyDismissedError, PasskeyNotRegisteredError } from '@touchque/web';
|
|
3
|
+
import * as react from 'react';
|
|
4
|
+
|
|
5
|
+
type PasskeyStatus = 'idle' | 'pending' | 'success' | 'stepup' | 'pending2fa' | 'error';
|
|
6
|
+
interface UseTouchQuePasskey {
|
|
7
|
+
/** `true` if this browser exposes the WebAuthn API. */
|
|
8
|
+
isSupported: boolean;
|
|
9
|
+
/** Current state of the last `authenticate()` call. */
|
|
10
|
+
status: PasskeyStatus;
|
|
11
|
+
/** The last error, or `null`. */
|
|
12
|
+
error: Error | null;
|
|
13
|
+
/** The last `authenticate()` result, or `null`. */
|
|
14
|
+
result: AuthenticateResult | null;
|
|
15
|
+
/** Passwordless-primary sign-in. Updates `status` / `error` / `result`. */
|
|
16
|
+
authenticate: (email: string, context?: Record<string, unknown>) => Promise<AuthenticateResult>;
|
|
17
|
+
/** Register a passkey for the signed-in user. */
|
|
18
|
+
register: (extra?: Record<string, unknown>) => Promise<RegisterResult>;
|
|
19
|
+
/** List the signed-in user's passkeys. */
|
|
20
|
+
list: () => Promise<PasskeySummary[]>;
|
|
21
|
+
/** Remove one of the signed-in user's passkeys. */
|
|
22
|
+
remove: (id: string) => Promise<{
|
|
23
|
+
deleted: boolean;
|
|
24
|
+
}>;
|
|
25
|
+
/** Reset `status` / `error` / `result` to their initial values. */
|
|
26
|
+
reset: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* React hook over `@touchque/web`. Memoizes one client for the given config
|
|
31
|
+
* and tracks the state of the passkey sign-in flow.
|
|
32
|
+
*
|
|
33
|
+
* ```tsx
|
|
34
|
+
* const pk = useTouchQuePasskey({ baseUrl: 'https://api.example.com' });
|
|
35
|
+
* <button disabled={!pk.isSupported} onClick={() => pk.authenticate(email)}>
|
|
36
|
+
* Sign in with a passkey
|
|
37
|
+
* </button>
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
declare function useTouchQuePasskey(config: TouchQueWebConfig): UseTouchQuePasskey;
|
|
41
|
+
|
|
42
|
+
interface PasskeyButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onError' | 'children'> {
|
|
43
|
+
/** `@touchque/web` config (`baseUrl`, `paths?`, `credentials?`, …). */
|
|
44
|
+
config: TouchQueWebConfig;
|
|
45
|
+
/** The account to sign in. */
|
|
46
|
+
email: string;
|
|
47
|
+
/** Called when the passkey assertion signed the user in. */
|
|
48
|
+
onSuccess?: (result: AuthenticateResult) => void;
|
|
49
|
+
/** Called when risk/policy wants a second factor (push / number match). */
|
|
50
|
+
onStepUp?: (result: AuthenticateResult) => void;
|
|
51
|
+
/** Called when the relay wants the caller to continue a pending 2FA flow. */
|
|
52
|
+
onPending2fa?: (result: AuthenticateResult) => void;
|
|
53
|
+
/** Called on any error (dismissed prompt, no passkey, disabled, network). */
|
|
54
|
+
onError?: (error: Error) => void;
|
|
55
|
+
/** Hide the button entirely when WebAuthn is unavailable. Default: false. */
|
|
56
|
+
hideWhenUnsupported?: boolean;
|
|
57
|
+
children?: React.ReactNode;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A button that runs the passwordless-primary passkey sign-in and routes the
|
|
61
|
+
* outcome to `onSuccess` / `onStepUp` / `onPending2fa` / `onError`. While the
|
|
62
|
+
* ceremony is running the button is `disabled` and `data-loading` is set.
|
|
63
|
+
*/
|
|
64
|
+
declare function PasskeyButton({ config, email, onSuccess, onStepUp, onPending2fa, onError, hideWhenUnsupported, children, disabled, onClick, ...buttonProps }: PasskeyButtonProps): react.JSX.Element | null;
|
|
65
|
+
|
|
66
|
+
export { PasskeyButton, type PasskeyButtonProps, type PasskeyStatus, type UseTouchQuePasskey, useTouchQuePasskey };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var web = require('@touchque/web');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
|
|
7
|
+
// src/useTouchQuePasskey.ts
|
|
8
|
+
function classify(res) {
|
|
9
|
+
if (res.ok) return "success";
|
|
10
|
+
if (res.requiresStepUp) return "stepup";
|
|
11
|
+
if (res.pending2fa) return "pending2fa";
|
|
12
|
+
return "error";
|
|
13
|
+
}
|
|
14
|
+
function useTouchQuePasskey(config) {
|
|
15
|
+
const client = react.useMemo(
|
|
16
|
+
() => web.createTouchQueWeb(config),
|
|
17
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
18
|
+
[config.baseUrl, config.credentials, JSON.stringify(config.paths ?? {})]
|
|
19
|
+
);
|
|
20
|
+
const [status, setStatus] = react.useState("idle");
|
|
21
|
+
const [error, setError] = react.useState(null);
|
|
22
|
+
const [result, setResult] = react.useState(null);
|
|
23
|
+
const inFlight = react.useRef(false);
|
|
24
|
+
const isSupported = react.useMemo(() => web.isPasskeySupported(), []);
|
|
25
|
+
const authenticate = react.useCallback(
|
|
26
|
+
async (email, context) => {
|
|
27
|
+
if (inFlight.current) throw new Error("A passkey sign-in is already in progress.");
|
|
28
|
+
inFlight.current = true;
|
|
29
|
+
setStatus("pending");
|
|
30
|
+
setError(null);
|
|
31
|
+
try {
|
|
32
|
+
const res = await client.passkeys.authenticate({ email, context });
|
|
33
|
+
setResult(res);
|
|
34
|
+
setStatus(classify(res));
|
|
35
|
+
return res;
|
|
36
|
+
} catch (e) {
|
|
37
|
+
setError(e);
|
|
38
|
+
setStatus("error");
|
|
39
|
+
throw e;
|
|
40
|
+
} finally {
|
|
41
|
+
inFlight.current = false;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
[client]
|
|
45
|
+
);
|
|
46
|
+
const register = react.useCallback(
|
|
47
|
+
(extra) => client.passkeys.register(extra),
|
|
48
|
+
[client]
|
|
49
|
+
);
|
|
50
|
+
const list = react.useCallback(() => client.passkeys.list(), [client]);
|
|
51
|
+
const remove = react.useCallback((id) => client.passkeys.remove(id), [client]);
|
|
52
|
+
const reset = react.useCallback(() => {
|
|
53
|
+
setStatus("idle");
|
|
54
|
+
setError(null);
|
|
55
|
+
setResult(null);
|
|
56
|
+
}, []);
|
|
57
|
+
return { isSupported, status, error, result, authenticate, register, list, remove, reset };
|
|
58
|
+
}
|
|
59
|
+
function PasskeyButton({
|
|
60
|
+
config,
|
|
61
|
+
email,
|
|
62
|
+
onSuccess,
|
|
63
|
+
onStepUp,
|
|
64
|
+
onPending2fa,
|
|
65
|
+
onError,
|
|
66
|
+
hideWhenUnsupported = false,
|
|
67
|
+
children,
|
|
68
|
+
disabled,
|
|
69
|
+
onClick,
|
|
70
|
+
...buttonProps
|
|
71
|
+
}) {
|
|
72
|
+
const pk = useTouchQuePasskey(config);
|
|
73
|
+
if (hideWhenUnsupported && !pk.isSupported) return null;
|
|
74
|
+
const busy = pk.status === "pending";
|
|
75
|
+
const handleClick = async (e) => {
|
|
76
|
+
onClick?.(e);
|
|
77
|
+
if (e.defaultPrevented || busy) return;
|
|
78
|
+
try {
|
|
79
|
+
const res = await pk.authenticate(email);
|
|
80
|
+
if (res.ok) onSuccess?.(res);
|
|
81
|
+
else if (res.requiresStepUp) onStepUp?.(res);
|
|
82
|
+
else if (res.pending2fa) onPending2fa?.(res);
|
|
83
|
+
else onError?.(new Error("Passkey sign-in did not complete."));
|
|
84
|
+
} catch (err) {
|
|
85
|
+
onError?.(err);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
89
|
+
"button",
|
|
90
|
+
{
|
|
91
|
+
type: "button",
|
|
92
|
+
...buttonProps,
|
|
93
|
+
disabled: disabled || busy || !pk.isSupported,
|
|
94
|
+
"data-loading": busy ? "" : void 0,
|
|
95
|
+
onClick: handleClick,
|
|
96
|
+
children: children ?? "Sign in with a passkey"
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
Object.defineProperty(exports, "PasskeyDisabledError", {
|
|
102
|
+
enumerable: true,
|
|
103
|
+
get: function () { return web.PasskeyDisabledError; }
|
|
104
|
+
});
|
|
105
|
+
Object.defineProperty(exports, "PasskeyDismissedError", {
|
|
106
|
+
enumerable: true,
|
|
107
|
+
get: function () { return web.PasskeyDismissedError; }
|
|
108
|
+
});
|
|
109
|
+
Object.defineProperty(exports, "PasskeyNotRegisteredError", {
|
|
110
|
+
enumerable: true,
|
|
111
|
+
get: function () { return web.PasskeyNotRegisteredError; }
|
|
112
|
+
});
|
|
113
|
+
exports.PasskeyButton = PasskeyButton;
|
|
114
|
+
exports.useTouchQuePasskey = useTouchQuePasskey;
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useMemo, useState, useRef, useCallback } from 'react';
|
|
2
|
+
import { createTouchQueWeb, isPasskeySupported } from '@touchque/web';
|
|
3
|
+
export { PasskeyDisabledError, PasskeyDismissedError, PasskeyNotRegisteredError } from '@touchque/web';
|
|
4
|
+
import { jsx } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
// src/useTouchQuePasskey.ts
|
|
7
|
+
function classify(res) {
|
|
8
|
+
if (res.ok) return "success";
|
|
9
|
+
if (res.requiresStepUp) return "stepup";
|
|
10
|
+
if (res.pending2fa) return "pending2fa";
|
|
11
|
+
return "error";
|
|
12
|
+
}
|
|
13
|
+
function useTouchQuePasskey(config) {
|
|
14
|
+
const client = useMemo(
|
|
15
|
+
() => createTouchQueWeb(config),
|
|
16
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
17
|
+
[config.baseUrl, config.credentials, JSON.stringify(config.paths ?? {})]
|
|
18
|
+
);
|
|
19
|
+
const [status, setStatus] = useState("idle");
|
|
20
|
+
const [error, setError] = useState(null);
|
|
21
|
+
const [result, setResult] = useState(null);
|
|
22
|
+
const inFlight = useRef(false);
|
|
23
|
+
const isSupported = useMemo(() => isPasskeySupported(), []);
|
|
24
|
+
const authenticate = useCallback(
|
|
25
|
+
async (email, context) => {
|
|
26
|
+
if (inFlight.current) throw new Error("A passkey sign-in is already in progress.");
|
|
27
|
+
inFlight.current = true;
|
|
28
|
+
setStatus("pending");
|
|
29
|
+
setError(null);
|
|
30
|
+
try {
|
|
31
|
+
const res = await client.passkeys.authenticate({ email, context });
|
|
32
|
+
setResult(res);
|
|
33
|
+
setStatus(classify(res));
|
|
34
|
+
return res;
|
|
35
|
+
} catch (e) {
|
|
36
|
+
setError(e);
|
|
37
|
+
setStatus("error");
|
|
38
|
+
throw e;
|
|
39
|
+
} finally {
|
|
40
|
+
inFlight.current = false;
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
[client]
|
|
44
|
+
);
|
|
45
|
+
const register = useCallback(
|
|
46
|
+
(extra) => client.passkeys.register(extra),
|
|
47
|
+
[client]
|
|
48
|
+
);
|
|
49
|
+
const list = useCallback(() => client.passkeys.list(), [client]);
|
|
50
|
+
const remove = useCallback((id) => client.passkeys.remove(id), [client]);
|
|
51
|
+
const reset = useCallback(() => {
|
|
52
|
+
setStatus("idle");
|
|
53
|
+
setError(null);
|
|
54
|
+
setResult(null);
|
|
55
|
+
}, []);
|
|
56
|
+
return { isSupported, status, error, result, authenticate, register, list, remove, reset };
|
|
57
|
+
}
|
|
58
|
+
function PasskeyButton({
|
|
59
|
+
config,
|
|
60
|
+
email,
|
|
61
|
+
onSuccess,
|
|
62
|
+
onStepUp,
|
|
63
|
+
onPending2fa,
|
|
64
|
+
onError,
|
|
65
|
+
hideWhenUnsupported = false,
|
|
66
|
+
children,
|
|
67
|
+
disabled,
|
|
68
|
+
onClick,
|
|
69
|
+
...buttonProps
|
|
70
|
+
}) {
|
|
71
|
+
const pk = useTouchQuePasskey(config);
|
|
72
|
+
if (hideWhenUnsupported && !pk.isSupported) return null;
|
|
73
|
+
const busy = pk.status === "pending";
|
|
74
|
+
const handleClick = async (e) => {
|
|
75
|
+
onClick?.(e);
|
|
76
|
+
if (e.defaultPrevented || busy) return;
|
|
77
|
+
try {
|
|
78
|
+
const res = await pk.authenticate(email);
|
|
79
|
+
if (res.ok) onSuccess?.(res);
|
|
80
|
+
else if (res.requiresStepUp) onStepUp?.(res);
|
|
81
|
+
else if (res.pending2fa) onPending2fa?.(res);
|
|
82
|
+
else onError?.(new Error("Passkey sign-in did not complete."));
|
|
83
|
+
} catch (err) {
|
|
84
|
+
onError?.(err);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
return /* @__PURE__ */ jsx(
|
|
88
|
+
"button",
|
|
89
|
+
{
|
|
90
|
+
type: "button",
|
|
91
|
+
...buttonProps,
|
|
92
|
+
disabled: disabled || busy || !pk.isSupported,
|
|
93
|
+
"data-loading": busy ? "" : void 0,
|
|
94
|
+
onClick: handleClick,
|
|
95
|
+
children: children ?? "Sign in with a passkey"
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { PasskeyButton, useTouchQuePasskey };
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@touchque/react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React bindings for @touchque/web — passkey sign-in hook and button",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/Touchque/touchque-sdks",
|
|
8
|
+
"directory": "touchque-react"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://touchque.com",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "TouchQue",
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"module": "./dist/index.mjs",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"import": {
|
|
19
|
+
"types": "./dist/index.d.mts",
|
|
20
|
+
"default": "./dist/index.mjs"
|
|
21
|
+
},
|
|
22
|
+
"require": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"CHANGELOG.md"
|
|
31
|
+
],
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup",
|
|
38
|
+
"dev": "tsup --watch",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"prepublishOnly": "npm run build && npm test"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"touchque",
|
|
44
|
+
"passkey",
|
|
45
|
+
"webauthn",
|
|
46
|
+
"passwordless",
|
|
47
|
+
"react",
|
|
48
|
+
"hooks"
|
|
49
|
+
],
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@touchque/web": "^0.1.0",
|
|
52
|
+
"react": ">=18"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@testing-library/react": "^16.1.0",
|
|
56
|
+
"@touchque/web": "file:../touchque-web",
|
|
57
|
+
"@types/react": "^18.3.12",
|
|
58
|
+
"jsdom": "^25.0.1",
|
|
59
|
+
"react": "^18.3.1",
|
|
60
|
+
"react-dom": "^18.3.1",
|
|
61
|
+
"tsup": "^8.0.2",
|
|
62
|
+
"typescript": "^5.4.5",
|
|
63
|
+
"vitest": "^2.1.4"
|
|
64
|
+
},
|
|
65
|
+
"publishConfig": {
|
|
66
|
+
"access": "public"
|
|
67
|
+
}
|
|
68
|
+
}
|