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[];