@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 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.
@@ -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 };
@@ -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
+ }