diff --git a/package-lock.json b/package-lock.json
index da78d07..0644908 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,7 @@
"version": "2.1.0",
"license": "MIT",
"dependencies": {
- "@authorizerdev/authorizer-js": "3.2.1",
+ "@authorizerdev/authorizer-js": "3.3.0-rc.0",
"@storybook/preset-scss": "^1.0.3",
"validator": "^13.11.0"
},
@@ -73,9 +73,9 @@
}
},
"node_modules/@authorizerdev/authorizer-js": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-3.2.1.tgz",
- "integrity": "sha512-Z7Vpdqs3JsosCcjV63rd7w7mj5n9Vh+RnGDR+qamTbbII+cbwttHpw1jP+61P2uNQLWC3jjp50HvL7xccWAnJQ==",
+ "version": "3.3.0-rc.0",
+ "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-3.3.0-rc.0.tgz",
+ "integrity": "sha512-P9S25JZpSqYR46YOT1W6BugfQkqHaswFLkUt4drJLPHH3vecRCUC0IczHMT2NkAbZzILoBh3LNOdV1BFd61CLw==",
"license": "MIT",
"dependencies": {
"cross-fetch": "^4.1.0"
diff --git a/package.json b/package.json
index cde4c17..19d3b67 100644
--- a/package.json
+++ b/package.json
@@ -91,7 +91,7 @@
"typescript": "^5.7.2"
},
"dependencies": {
- "@authorizerdev/authorizer-js": "3.2.1",
+ "@authorizerdev/authorizer-js": "3.3.0-rc.0",
"@storybook/preset-scss": "^1.0.3",
"validator": "^13.11.0"
}
diff --git a/src/components/AuthorizerPasskeyLogin.tsx b/src/components/AuthorizerPasskeyLogin.tsx
new file mode 100644
index 0000000..0df930c
--- /dev/null
+++ b/src/components/AuthorizerPasskeyLogin.tsx
@@ -0,0 +1,94 @@
+import { FC, useState } from 'react';
+import { AuthToken, isWebauthnSupported } from '@authorizerdev/authorizer-js';
+
+import '../styles/default.css';
+import { ButtonAppearance, MessageType } from '../constants';
+import { useAuthorizer } from '../contexts/AuthorizerContext';
+import { StyledButton, StyledSeparator } from '../styledComponents';
+import { Message } from './Message';
+
+// AuthorizerPasskeyLogin offers a full passwordless, usernameless "Sign in
+// with a passkey" option (discoverable-credential login) alongside the other
+// login methods. It only renders when the browser actually supports the
+// WebAuthn JSON ceremony APIs the SDK relies on - there is no server-side
+// config flag for passkeys (unlike social login), it's purely a browser
+// capability.
+export const AuthorizerPasskeyLogin: FC<{
+ onLogin?: (data: AuthToken | void) => void;
+}> = ({ onLogin }) => {
+ const [error, setError] = useState(``);
+ const [loading, setLoading] = useState(false);
+ const { setAuthData, config, authorizerRef } = useAuthorizer();
+
+ if (!isWebauthnSupported()) {
+ return null;
+ }
+
+ // Only show the "OR" separator if AuthorizerRoot is actually going to
+ // render something below it - otherwise the passkey button ends up
+ // followed by a trailing separator with nothing underneath. Mirrors the
+ // exact set of conditions AuthorizerRoot uses to decide whether
+ // AuthorizerSocialLogin, AuthorizerBasicAuthLogin, or
+ // AuthorizerMagicLinkLogin render anything on the login view.
+ const hasSocialLogin =
+ config.is_google_login_enabled ||
+ config.is_github_login_enabled ||
+ config.is_facebook_login_enabled ||
+ config.is_linkedin_login_enabled ||
+ config.is_apple_login_enabled ||
+ config.is_twitter_login_enabled ||
+ config.is_microsoft_login_enabled ||
+ config.is_twitch_login_enabled ||
+ config.is_roblox_login_enabled;
+ const hasBasicAuthLogin =
+ (config.is_basic_authentication_enabled ||
+ config.is_mobile_basic_authentication_enabled) &&
+ !config.is_magic_link_login_enabled;
+ const hasAnotherLoginMethod =
+ hasSocialLogin || hasBasicAuthLogin || config.is_magic_link_login_enabled;
+
+ const onClick = async () => {
+ setError(``);
+ try {
+ setLoading(true);
+ const { data: res, errors } = await authorizerRef.loginWithPasskey();
+ if (errors && errors.length) {
+ setError(errors[0]?.message || ``);
+ return;
+ }
+ if (res) {
+ setAuthData({
+ user: res.user || null,
+ token: res,
+ config,
+ loading: false,
+ });
+ }
+ if (onLogin) {
+ onLogin(res);
+ }
+ } catch (err) {
+ setError((err as Error).message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const onErrorClose = () => setError(``);
+
+ return (
+ <>
+ {error && (
+
+ )}
+
+ {loading ? `Waiting for passkey ...` : `Sign in with a passkey`}
+
+ {hasAnotherLoginMethod && OR}
+ >
+ );
+};
diff --git a/src/components/AuthorizerRoot.tsx b/src/components/AuthorizerRoot.tsx
index ff117b3..7c2e9c8 100644
--- a/src/components/AuthorizerRoot.tsx
+++ b/src/components/AuthorizerRoot.tsx
@@ -9,6 +9,7 @@ import { AuthorizerSignup } from './AuthorizerSignup';
import type { FormFieldsOverrides } from './AuthorizerSignup';
import { AuthorizerForgotPassword } from './AuthorizerForgotPassword';
import { AuthorizerSocialLogin } from './AuthorizerSocialLogin';
+import { AuthorizerPasskeyLogin } from './AuthorizerPasskeyLogin';
import { AuthorizerMagicLinkLogin } from './AuthorizerMagicLinkLogin';
import { createRandomString } from '../utils/common';
import { hasWindow } from '../utils/window';
@@ -60,6 +61,7 @@ export const AuthorizerRoot: FC<{
return (
+ {view === Views.Login && }
{view === Views.Login &&
(config.is_basic_authentication_enabled ||
config.is_mobile_basic_authentication_enabled) &&
diff --git a/src/index.tsx b/src/index.tsx
index c021138..0c375b3 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -14,6 +14,7 @@ import { AuthorizerResetPassword } from './components/AuthorizerResetPassword';
import { AuthorizerVerifyOtp } from './components/AuthorizerVerifyOtp';
import { AuthorizerRoot as Authorizer } from './components/AuthorizerRoot';
import { AuthorizerTOTPScanner } from './components/AuthorizerTOTPScanner';
+import { AuthorizerPasskeyLogin } from './components/AuthorizerPasskeyLogin';
export {
useAuthorizer,
@@ -27,4 +28,5 @@ export {
AuthorizerResetPassword,
AuthorizerVerifyOtp,
AuthorizerTOTPScanner,
+ AuthorizerPasskeyLogin,
};