diff --git a/.changeset/weak-adults-juggle.md b/.changeset/weak-adults-juggle.md
new file mode 100644
index 00000000000..ef3829005fd
--- /dev/null
+++ b/.changeset/weak-adults-juggle.md
@@ -0,0 +1,6 @@
+---
+'@clerk/clerk-js': minor
+'@clerk/types': minor
+---
+
+Allow users to authenticate with passkeys via the ``.
diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts
index 9fef4a38c58..9672d93afa9 100644
--- a/packages/clerk-js/src/core/resources/SignIn.ts
+++ b/packages/clerk-js/src/core/resources/SignIn.ts
@@ -261,7 +261,11 @@ export class SignIn extends BaseResource implements SignInResource {
});
};
- public __experimental_authenticateWithPasskey = async (): Promise => {
+ public __experimental_authenticateWithPasskey = async (params?: {
+ flow?: 'autofill' | 'discoverable';
+ }): Promise => {
+ const { flow } = params || {};
+
/**
* The UI should always prevent from this method being called if WebAuthn is not supported.
* As a precaution we need to check if WebAuthn is supported.
@@ -272,24 +276,23 @@ export class SignIn extends BaseResource implements SignInResource {
});
}
- if (!this.firstFactorVerification.nonce) {
+ if (flow === 'autofill' || flow === 'discoverable') {
// @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
await this.create({ strategy: 'passkey' });
- }
-
- // @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
- const passKeyFactor = this.supportedFirstFactors.find(
+ } else {
// @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
- f => f.strategy === 'passkey',
- ) as __experimental_PasskeyFactor;
+ const passKeyFactor = this.supportedFirstFactors.find(
+ // @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
+ f => f.strategy === 'passkey',
+ ) as __experimental_PasskeyFactor;
- if (!passKeyFactor) {
- clerkVerifyPasskeyCalledBeforeCreate();
+ if (!passKeyFactor) {
+ clerkVerifyPasskeyCalledBeforeCreate();
+ }
+ // @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
+ await this.prepareFirstFactor(passKeyFactor);
}
- // @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
- await this.prepareFirstFactor(passKeyFactor);
-
const { nonce } = this.firstFactorVerification;
const publicKey = nonce ? convertJSONToPublicKeyRequestOptions(JSON.parse(nonce)) : null;
@@ -298,10 +301,20 @@ export class SignIn extends BaseResource implements SignInResource {
throw 'Missing key';
}
+ let canUseConditionalUI = false;
+
+ if (flow === 'autofill') {
+ /**
+ * If autofill is not supported gracefully handle the result, we don't need to throw.
+ * The caller should always check this before calling this method.
+ */
+ canUseConditionalUI = await isWebAuthnAutofillSupported();
+ }
+
// Invoke the WebAuthn get() method.
const { publicKeyCredential, error } = await webAuthnGetCredential({
publicKeyOptions: publicKey,
- conditionalUI: await isWebAuthnAutofillSupported(),
+ conditionalUI: canUseConditionalUI,
});
if (!publicKeyCredential) {
diff --git a/packages/clerk-js/src/core/resources/UserSettings.ts b/packages/clerk-js/src/core/resources/UserSettings.ts
index 34106038f5a..fa2248270ec 100644
--- a/packages/clerk-js/src/core/resources/UserSettings.ts
+++ b/packages/clerk-js/src/core/resources/UserSettings.ts
@@ -2,6 +2,7 @@ import type {
Attributes,
OAuthProviders,
OAuthStrategy,
+ PasskeySettingsData,
PasswordSettingsData,
SamlSettings,
SignInData,
@@ -35,6 +36,7 @@ export class UserSettings extends BaseResource implements UserSettingsResource {
signIn!: SignInData;
signUp!: SignUpData;
passwordSettings!: PasswordSettingsData;
+ passkeySettings!: PasskeySettingsData;
socialProviderStrategies: OAuthStrategy[] = [];
authenticatableSocialStrategies: OAuthStrategy[] = [];
@@ -79,6 +81,7 @@ export class UserSettings extends BaseResource implements UserSettingsResource {
? defaultMaxPasswordLength
: Math.min(data?.password_settings?.max_length, defaultMaxPasswordLength),
};
+ this.passkeySettings = data.passkey_settings;
this.socialProviderStrategies = this.getSocialProviderStrategies(data.social);
this.authenticatableSocialStrategies = this.getAuthenticatableSocialStrategies(data.social);
this.web3FirstFactors = this.getWeb3FirstFactors(this.attributes);
diff --git a/packages/clerk-js/src/ui/common/constants.ts b/packages/clerk-js/src/ui/common/constants.ts
index 37a7cf642ba..ecd355a07c9 100644
--- a/packages/clerk-js/src/ui/common/constants.ts
+++ b/packages/clerk-js/src/ui/common/constants.ts
@@ -72,6 +72,7 @@ export const getIdentifierControlDisplayValues = (
};
export const PREFERRED_SIGN_IN_STRATEGIES = Object.freeze({
+ Passkey: 'passkey',
Password: 'password',
OTP: 'otp',
});
diff --git a/packages/clerk-js/src/ui/components/SignIn/AlternativeMethods.tsx b/packages/clerk-js/src/ui/components/SignIn/AlternativeMethods.tsx
index 26bb83cc27e..4cc19d33637 100644
--- a/packages/clerk-js/src/ui/components/SignIn/AlternativeMethods.tsx
+++ b/packages/clerk-js/src/ui/components/SignIn/AlternativeMethods.tsx
@@ -6,7 +6,7 @@ import { Button, Col, descriptors, Flex, Flow, localizationKeys } from '../../cu
import { ArrowBlockButton, BackLink, Card, Divider, Header } from '../../elements';
import { useCardState } from '../../elements/contexts';
import { useAlternativeStrategies } from '../../hooks/useAlternativeStrategies';
-import { ChatAltIcon, Email, LinkIcon, LockClosedIcon, RequestAuthIcon } from '../../icons';
+import { ChatAltIcon, Email, Fingerprint, LinkIcon, LockClosedIcon, RequestAuthIcon } from '../../icons';
import { formatSafeIdentifier } from '../../utils';
import { SignInSocialButtons } from './SignInSocialButtons';
import { useResetPasswordFactor } from './useResetPasswordFactor';
@@ -136,6 +136,9 @@ export function getButtonLabel(factor: SignInFactor): LocalizationKey {
});
case 'password':
return localizationKeys('signIn.alternativeMethods.blockButton__password');
+ // @ts-ignore
+ case 'passkey':
+ return localizationKeys('signIn.alternativeMethods.blockButton__passkey');
case 'reset_password_email_code':
return localizationKeys('signIn.forgotPasswordAlternativeMethods.blockButton__resetPassword');
case 'reset_password_phone_code':
@@ -153,6 +156,7 @@ export function getButtonIcon(factor: SignInFactor) {
reset_password_email_code: RequestAuthIcon,
reset_password_phone_code: RequestAuthIcon,
password: LockClosedIcon,
+ passkey: Fingerprint,
} as const;
return icons[factor.strategy as keyof typeof icons];
diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx
index bbf2d8544b0..579136ad533 100644
--- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx
+++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx
@@ -11,6 +11,7 @@ import { AlternativeMethods } from './AlternativeMethods';
import { SignInFactorOneEmailCodeCard } from './SignInFactorOneEmailCodeCard';
import { SignInFactorOneEmailLinkCard } from './SignInFactorOneEmailLinkCard';
import { SignInFactorOneForgotPasswordCard } from './SignInFactorOneForgotPasswordCard';
+import { SignInFactorOnePasskey } from './SignInFactorOnePasskey';
import { SignInFactorOnePasswordCard } from './SignInFactorOnePasswordCard';
import { SignInFactorOnePhoneCodeCard } from './SignInFactorOnePhoneCodeCard';
import { useResetPasswordFactor } from './useResetPasswordFactor';
@@ -112,6 +113,14 @@ export function _SignInFactorOne(): JSX.Element {
}
switch (currentFactor?.strategy) {
+ // @ts-ignore
+ case 'passkey':
+ return (
+
+ );
case 'password':
return (
void;
+};
+
+export const SignInFactorOnePasskey = (props: SignInFactorOnePasswordProps) => {
+ const { onShowAlternativeMethodsClick } = props;
+ const card = useCardState();
+ const signIn = useCoreSignIn();
+ const { navigate } = useRouter();
+ const [showHavingTrouble, setShowHavingTrouble] = React.useState(false);
+ const toggleHavingTrouble = React.useCallback(() => setShowHavingTrouble(s => !s), [setShowHavingTrouble]);
+ const authenticateWithPasskey = useHandleAuthenticateWithPasskey();
+
+ const goBack = () => {
+ return navigate('../');
+ };
+
+ const handleSubmit: React.FormEventHandler = e => {
+ e.preventDefault();
+ return authenticateWithPasskey();
+ };
+
+ if (showHavingTrouble) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ ({
+ color: t.colors.$neutralAlpha500,
+ marginInline: 'auto',
+ paddingBottom: t.sizes.$1,
+ width: t.sizes.$12,
+ height: t.sizes.$12,
+ })}
+ />
+
+
+
+
+ {card.error}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx
index ccfe726fc4d..aa8b07d97b1 100644
--- a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx
+++ b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx
@@ -5,6 +5,7 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { ERROR_CODES } from '../../../core/constants';
import { clerkInvalidFAPIResponse } from '../../../core/errors';
import { getClerkQueryParam, removeClerkQueryParam } from '../../../utils';
+import { isWebAuthnAutofillSupported, isWebAuthnSupported } from '../../../utils/passkeys';
import type { SignInStartIdentifier } from '../../common';
import { getIdentifierControlDisplayValues, groupIdentifiers, withRedirectToAfterSignIn } from '../../common';
import { buildSSOCallbackURL } from '../../common/redirects';
@@ -24,8 +25,36 @@ import { useSupportEmail } from '../../hooks/useSupportEmail';
import { useRouter } from '../../router';
import type { FormControlState } from '../../utils';
import { buildRequest, handleError, isMobileDevice, useFormControl } from '../../utils';
+import { useHandleAuthenticateWithPasskey } from './shared';
import { SignInSocialButtons } from './SignInSocialButtons';
+const useAutoFillPasskey = () => {
+ const [isSupported, setIsSupported] = useState(false);
+ const authenticateWithPasskey = useHandleAuthenticateWithPasskey();
+ const { userSettings } = useEnvironment();
+ const { passkeySettings } = userSettings;
+
+ useEffect(() => {
+ async function runAutofillPasskey() {
+ const _isSupported = await isWebAuthnAutofillSupported();
+ setIsSupported(_isSupported);
+ if (!_isSupported) {
+ return;
+ }
+
+ await authenticateWithPasskey({ flow: 'autofill' });
+ }
+
+ if (passkeySettings.allow_autofill) {
+ runAutofillPasskey();
+ }
+ }, []);
+
+ return {
+ isWebAuthnAutofillSupported: isSupported,
+ };
+};
+
export function _SignInStart(): JSX.Element {
const card = useCardState();
const clerk = useClerk();
@@ -41,6 +70,13 @@ export function _SignInStart(): JSX.Element {
[userSettings.enabledFirstFactorIdentifiers],
);
+ /**
+ * Passkeys
+ */
+ const { isWebAuthnAutofillSupported } = useAutoFillPasskey();
+ const authenticateWithPasskey = useHandleAuthenticateWithPasskey();
+ const isWebSupported = isWebAuthnSupported();
+
const onlyPhoneNumberInitialValueExists =
!!ctx.initialValues?.phoneNumber && !(ctx.initialValues.emailAddress || ctx.initialValues.username);
const shouldStartWithPhoneNumberIdentifier =
@@ -318,6 +354,7 @@ export function _SignInStart(): JSX.Element {
onActionClicked={switchToNextIdentifier}
{...identifierFieldProps}
autoFocus={shouldAutofocus}
+ autoComplete={isWebAuthnAutofillSupported ? 'webauthn' : undefined}
/>
@@ -326,9 +363,16 @@ export function _SignInStart(): JSX.Element {
) : null}
+ {userSettings.passkeySettings.show_sign_in_button && isWebSupported && (
+
+ authenticateWithPasskey({ flow: 'discoverable' })}
+ />
+
+ )}
-
diff --git a/packages/clerk-js/src/ui/components/SignIn/shared.ts b/packages/clerk-js/src/ui/components/SignIn/shared.ts
new file mode 100644
index 00000000000..3dcfcc7ecbd
--- /dev/null
+++ b/packages/clerk-js/src/ui/components/SignIn/shared.ts
@@ -0,0 +1,52 @@
+import { isClerkRuntimeError, isUserLockedError } from '@clerk/shared/error';
+import { useClerk } from '@clerk/shared/react';
+import { useCallback, useEffect } from 'react';
+
+import { clerkInvalidFAPIResponse } from '../../../core/errors';
+import { __internal_WebAuthnAbortService } from '../../../utils/passkeys';
+import { useCoreSignIn, useSignInContext } from '../../contexts';
+import { useCardState } from '../../elements';
+import { useSupportEmail } from '../../hooks/useSupportEmail';
+import { useRouter } from '../../router';
+import { handleError } from '../../utils';
+
+function useHandleAuthenticateWithPasskey() {
+ const card = useCardState();
+ const { setActive } = useClerk();
+ const { navigate } = useRouter();
+ const supportEmail = useSupportEmail();
+ const { navigateAfterSignIn } = useSignInContext();
+ const { __experimental_authenticateWithPasskey } = useCoreSignIn();
+
+ useEffect(() => {
+ return () => {
+ __internal_WebAuthnAbortService.abort();
+ };
+ }, []);
+
+ return useCallback(async (...args: Parameters) => {
+ try {
+ const res = await __experimental_authenticateWithPasskey(...args);
+ switch (res.status) {
+ case 'complete':
+ return setActive({ session: res.createdSessionId, beforeEmit: navigateAfterSignIn });
+ case 'needs_second_factor':
+ return navigate('../factor-two');
+ default:
+ return console.error(clerkInvalidFAPIResponse(res.status, supportEmail));
+ }
+ } catch (err) {
+ // In case of autofill, if retrieval of credentials is aborted just return to avoid updating state of unmounted components.
+ if (isClerkRuntimeError(err) && err.code === 'passkey_retrieval_aborted') {
+ return;
+ }
+ if (isUserLockedError(err)) {
+ // @ts-expect-error -- private method for the time being
+ return clerk.__internal_navigateWithError('..', err.errors[0]);
+ }
+ handleError(err, [], card.setError);
+ }
+ }, []);
+}
+
+export { useHandleAuthenticateWithPasskey };
diff --git a/packages/clerk-js/src/ui/components/SignIn/utils.ts b/packages/clerk-js/src/ui/components/SignIn/utils.ts
index cbd90311c91..90826ad0b34 100644
--- a/packages/clerk-js/src/ui/components/SignIn/utils.ts
+++ b/packages/clerk-js/src/ui/components/SignIn/utils.ts
@@ -90,6 +90,11 @@ export function determineStartingSignInFactor(
return null;
}
+ //TODO: Create proper function like `determineStrategyWhenOTPIsPreferred`
+ if (preferredSignInStrategy === PREFERRED_SIGN_IN_STRATEGIES.Passkey) {
+ // @ts-ignore
+ return firstFactors.find(f => f.strategy === PREFERRED_SIGN_IN_STRATEGIES.Passkey);
+ }
return preferredSignInStrategy === PREFERRED_SIGN_IN_STRATEGIES.Password
? determineStrategyWhenPasswordIsPreferred(firstFactors, identifier)
: determineStrategyWhenOTPIsPreferred(firstFactors, identifier);
@@ -103,7 +108,8 @@ export function determineSalutation(signIn: Partial): string {
return titleize(signIn.userData?.firstName) || titleize(signIn.userData?.lastName) || signIn?.identifier || '';
}
-const localStrategies: SignInStrategy[] = ['email_code', 'password', 'phone_code', 'email_link'];
+// @ts-ignore
+const localStrategies: SignInStrategy[] = ['passkey', 'email_code', 'password', 'phone_code', 'email_link'];
export function factorHasLocalStrategy(factor: SignInFactor | undefined | null): boolean {
if (!factor) {
diff --git a/packages/clerk-js/src/ui/customizables/elementDescriptors.ts b/packages/clerk-js/src/ui/customizables/elementDescriptors.ts
index 0808c5e9727..47ca61dbe2c 100644
--- a/packages/clerk-js/src/ui/customizables/elementDescriptors.ts
+++ b/packages/clerk-js/src/ui/customizables/elementDescriptors.ts
@@ -172,6 +172,8 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([
'identityPreviewEditButton',
'identityPreviewEditButtonIcon',
+ 'passkeyIcon',
+
'accountSwitcherActionButton',
'accountSwitcherActionButtonIconBox',
'accountSwitcherActionButtonIcon',
diff --git a/packages/clerk-js/src/ui/icons/fingerprint.svg b/packages/clerk-js/src/ui/icons/fingerprint.svg
new file mode 100644
index 00000000000..00f3e80bb3e
--- /dev/null
+++ b/packages/clerk-js/src/ui/icons/fingerprint.svg
@@ -0,0 +1,4 @@
+
diff --git a/packages/clerk-js/src/ui/icons/index.ts b/packages/clerk-js/src/ui/icons/index.ts
index 4794298d97a..785586f88ab 100644
--- a/packages/clerk-js/src/ui/icons/index.ts
+++ b/packages/clerk-js/src/ui/icons/index.ts
@@ -60,3 +60,4 @@ export { default as CaretLeft } from './caret-left.svg';
export { default as CaretRight } from './caret-right.svg';
export { default as Organization } from './organization.svg';
export { default as Users } from './users.svg';
+export { default as Fingerprint } from './fingerprint.svg';
diff --git a/packages/clerk-js/src/ui/utils/factorSorting.ts b/packages/clerk-js/src/ui/utils/factorSorting.ts
index d2569d10dfb..4f93e33db8d 100644
--- a/packages/clerk-js/src/ui/utils/factorSorting.ts
+++ b/packages/clerk-js/src/ui/utils/factorSorting.ts
@@ -7,6 +7,7 @@ const makeSortingOrderMap = (arr: T[]): Record =>
}, {} as Record);
const STRATEGY_SORT_ORDER_PASSWORD_PREF = makeSortingOrderMap([
+ 'passkey',
'password',
'email_link',
'email_code',
@@ -17,6 +18,7 @@ const STRATEGY_SORT_ORDER_OTP_PREF = makeSortingOrderMap([
'email_link',
'email_code',
'phone_code',
+ 'passkey',
'password',
] as SignInStrategy[]);
@@ -24,6 +26,7 @@ const STRATEGY_SORT_ORDER_ALL_STRATEGIES_BUTTONS = makeSortingOrderMap([
'email_link',
'email_code',
'phone_code',
+ 'passkey',
'password',
] as SignInStrategy[]);
diff --git a/packages/clerk-js/src/ui/utils/test/fixtures.ts b/packages/clerk-js/src/ui/utils/test/fixtures.ts
index 7520f9b7707..d32051038b7 100644
--- a/packages/clerk-js/src/ui/utils/test/fixtures.ts
+++ b/packages/clerk-js/src/ui/utils/test/fixtures.ts
@@ -169,6 +169,11 @@ const createBaseUserSettings = (): UserSettingsJSON => {
min_zxcvbn_strength: 0,
} as UserSettingsJSON['password_settings'];
+ const passkeySettingsConfig = {
+ allow_autofill: false,
+ show_sign_in_button: false,
+ } as UserSettingsJSON['passkey_settings'];
+
return {
attributes: { ...attributeConfig },
actions: { delete_self: false, create_organization: false },
@@ -193,6 +198,7 @@ const createBaseUserSettings = (): UserSettingsJSON => {
},
},
password_settings: passwordSettingsConfig,
+ passkey_settings: passkeySettingsConfig,
};
};
diff --git a/packages/clerk-js/src/utils/passkeys.ts b/packages/clerk-js/src/utils/passkeys.ts
index cb27fb2059a..fdca7ae6f6f 100644
--- a/packages/clerk-js/src/utils/passkeys.ts
+++ b/packages/clerk-js/src/utils/passkeys.ts
@@ -26,6 +26,8 @@ type WebAuthnGetCredentialReturn =
type ClerkWebAuthnErrorCode =
| 'passkey_exists'
+ | 'passkey_retrieval_aborted'
+ | 'passkey_retrieval_cancelled'
| 'passkey_registration_cancelled'
| 'passkey_credential_create_failed'
| 'passkey_credential_get_failed';
@@ -102,6 +104,33 @@ async function webAuthnCreateCredential(
}
}
+class WebAuthnAbortService {
+ private controller: AbortController | undefined;
+
+ private __abort() {
+ if (!this.controller) {
+ return;
+ }
+ const abortError = new Error();
+ abortError.name = 'AbortError';
+ this.controller.abort(abortError);
+ }
+
+ createAbortSignal() {
+ this.__abort();
+ const newController = new AbortController();
+ this.controller = newController;
+ return newController.signal;
+ }
+
+ abort() {
+ this.__abort();
+ this.controller = undefined;
+ }
+}
+
+const __internal_WebAuthnAbortService = new WebAuthnAbortService();
+
async function webAuthnGetCredential({
publicKeyOptions,
conditionalUI,
@@ -114,6 +143,7 @@ async function webAuthnGetCredential({
const credential = (await navigator.credentials.get({
publicKey: publicKeyOptions,
mediation: conditionalUI ? 'conditional' : 'optional',
+ signal: __internal_WebAuthnAbortService.createAbortSignal(),
})) as __experimental_PublicKeyCredentialWithAuthenticatorAssertionResponse | null;
if (!credential) {
@@ -135,6 +165,7 @@ async function webAuthnGetCredential({
*/
function handlePublicKeyCreateError(error: Error): ClerkWebAuthnError | ClerkRuntimeError | Error {
if (error.name === 'InvalidStateError') {
+ // Note: Firefox will throw 'NotAllowedError' when passkeys exists
return new ClerkWebAuthnError(error.message, { code: 'passkey_exists' });
} else if (error.name === 'NotAllowedError') {
return new ClerkWebAuthnError(error.message, { code: 'passkey_registration_cancelled' });
@@ -147,8 +178,12 @@ function handlePublicKeyCreateError(error: Error): ClerkWebAuthnError | ClerkRun
* @param error
*/
function handlePublicKeyGetError(error: Error): ClerkWebAuthnError | ClerkRuntimeError | Error {
+ if (error.name === 'AbortError') {
+ return new ClerkWebAuthnError(error.message, { code: 'passkey_retrieval_aborted' });
+ }
+
if (error.name === 'NotAllowedError') {
- return new ClerkWebAuthnError(error.message, { code: 'passkey_registration_cancelled' });
+ return new ClerkWebAuthnError(error.message, { code: 'passkey_retrieval_cancelled' });
}
return error;
}
@@ -252,4 +287,5 @@ export {
convertJSONToPublicKeyRequestOptions,
serializePublicKeyCredential,
serializePublicKeyCredentialAssertion,
+ __internal_WebAuthnAbortService,
};
diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts
index 12a84557c95..61faa01a540 100644
--- a/packages/localizations/src/en-US.ts
+++ b/packages/localizations/src/en-US.ts
@@ -271,6 +271,7 @@ export const enUS: LocalizationResource = {
blockButton__emailCode: 'Email code to {{identifier}}',
blockButton__emailLink: 'Email link to {{identifier}}',
blockButton__password: 'Sign in with your password',
+ blockButton__passkey: 'Sign in with your passkey',
blockButton__phoneCode: 'Send SMS code to {{identifier}}',
blockButton__totp: 'Use your authenticator app',
getHelp: {
@@ -346,6 +347,10 @@ export const enUS: LocalizationResource = {
subtitle: 'Enter the password associated with your account',
title: 'Enter your password',
},
+ passkey: {
+ title: 'Use your passkey',
+ subtitle: "Using your passkey confirms it's you. Your device may ask for your fingerprint, face or screen lock.",
+ },
phoneCode: {
formTitle: 'Verification code',
resendButton: "Didn't receive a code? Resend",
@@ -371,6 +376,7 @@ export const enUS: LocalizationResource = {
actionLink: 'Sign up',
actionLink__use_email: 'Use email',
actionLink__use_email_username: 'Use email or username',
+ actionLink__use_passkey: 'Use passkey instead',
actionLink__use_phone: 'Use phone',
actionLink__use_username: 'Use username',
actionText: 'Don’t have an account?',
diff --git a/packages/types/src/appearance.ts b/packages/types/src/appearance.ts
index d0fb54a686d..a9d7c7c7674 100644
--- a/packages/types/src/appearance.ts
+++ b/packages/types/src/appearance.ts
@@ -296,6 +296,8 @@ export type ElementsConfig = {
identityPreviewEditButton: WithOptions;
identityPreviewEditButtonIcon: WithOptions;
+ passkeyIcon: WithOptions<'firstFactor'>;
+
accountSwitcherActionButton: WithOptions<'addAccount' | 'signOutAll'>;
accountSwitcherActionButtonIconBox: WithOptions<'addAccount' | 'signOutAll'>;
accountSwitcherActionButtonIcon: WithOptions<'addAccount' | 'signOutAll'>;
diff --git a/packages/types/src/displayConfig.ts b/packages/types/src/displayConfig.ts
index 36228251d0f..ded4d68089d 100644
--- a/packages/types/src/displayConfig.ts
+++ b/packages/types/src/displayConfig.ts
@@ -1,7 +1,7 @@
import type { DisplayThemeJSON } from './json';
import type { ClerkResource } from './resource';
-export type PreferredSignInStrategy = 'password' | 'otp';
+export type PreferredSignInStrategy = 'passkey' | 'password' | 'otp';
export interface DisplayConfigJSON {
object: 'display_config';
diff --git a/packages/types/src/elementIds.ts b/packages/types/src/elementIds.ts
index c15292fce10..d07c2918926 100644
--- a/packages/types/src/elementIds.ts
+++ b/packages/types/src/elementIds.ts
@@ -47,7 +47,7 @@ export type OrganizationPreviewId =
| 'organizationSwitcherListedOrganization'
| 'organizationSwitcherActiveOrganization';
-export type CardActionId = 'havingTrouble' | 'alternativeMethods' | 'signUp' | 'signIn';
+export type CardActionId = 'havingTrouble' | 'alternativeMethods' | 'signUp' | 'signIn' | 'usePasskey';
export type MenuId = 'invitation' | 'member' | ProfileSectionId;
export type SelectId = 'countryCode' | 'role';
diff --git a/packages/types/src/localization.ts b/packages/types/src/localization.ts
index a4ac7110e2f..8f2b234e256 100644
--- a/packages/types/src/localization.ts
+++ b/packages/types/src/localization.ts
@@ -150,12 +150,17 @@ type _LocalizationResource = {
actionLink__use_phone: LocalizationValue;
actionLink__use_username: LocalizationValue;
actionLink__use_email_username: LocalizationValue;
+ actionLink__use_passkey: LocalizationValue;
};
password: {
title: LocalizationValue;
subtitle: LocalizationValue;
actionLink: LocalizationValue;
};
+ passkey: {
+ title: LocalizationValue;
+ subtitle: LocalizationValue;
+ };
forgotPasswordAlternativeMethods: {
title: LocalizationValue;
label__alternativeMethods: LocalizationValue;
@@ -245,6 +250,7 @@ type _LocalizationResource = {
blockButton__emailCode: LocalizationValue;
blockButton__phoneCode: LocalizationValue;
blockButton__password: LocalizationValue;
+ blockButton__passkey: LocalizationValue;
blockButton__totp: LocalizationValue;
blockButton__backupCode: LocalizationValue;
getHelp: {
diff --git a/packages/types/src/signIn.ts b/packages/types/src/signIn.ts
index b0fd06dc27a..818c263124d 100644
--- a/packages/types/src/signIn.ts
+++ b/packages/types/src/signIn.ts
@@ -90,7 +90,7 @@ export interface SignInResource extends ClerkResource {
authenticateWithMetamask: () => Promise;
- __experimental_authenticateWithPasskey: () => Promise;
+ __experimental_authenticateWithPasskey: (params?: { flow?: 'autofill' | 'discoverable' }) => Promise;
createEmailLinkFlow: () => CreateEmailLinkFlowReturn;
diff --git a/packages/types/src/userSettings.ts b/packages/types/src/userSettings.ts
index e5b917b227d..948a38d9880 100644
--- a/packages/types/src/userSettings.ts
+++ b/packages/types/src/userSettings.ts
@@ -64,6 +64,11 @@ export type PasswordSettingsData = {
min_zxcvbn_strength: number;
};
+export type PasskeySettingsData = {
+ allow_autofill: boolean;
+ show_sign_in_button: boolean;
+};
+
export type OAuthProviders = {
[provider in OAuthStrategy]: OAuthProviderSettings;
};
@@ -97,6 +102,7 @@ export interface UserSettingsJSON extends ClerkResourceJSON {
sign_in: SignInData;
sign_up: SignUpData;
password_settings: PasswordSettingsData;
+ passkey_settings: PasskeySettingsData;
}
export interface UserSettingsResource extends ClerkResource {
@@ -110,6 +116,7 @@ export interface UserSettingsResource extends ClerkResource {
signIn: SignInData;
signUp: SignUpData;
passwordSettings: PasswordSettingsData;
+ passkeySettings: PasskeySettingsData;
socialProviderStrategies: OAuthStrategy[];
authenticatableSocialStrategies: OAuthStrategy[];
web3FirstFactors: Web3Strategy[];