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, };