Skip to content

Commit

Permalink
chore(clerk-js): Remove experimental prefixes for Passkey related apis (
Browse files Browse the repository at this point in the history
#3134)

* chore(clerk-js): Remove experimental prefixes for Passkey related apis

A few top level experimental still exist in order to ensure that we want break beta testers. Those will be removed soon.

* chore(clerk-js): Add stable localization keys for passkeys

* chore(clerk-js): Add changeset
  • Loading branch information
panteliselef authored Apr 8, 2024
1 parent 5c90b21 commit 2352149
Show file tree
Hide file tree
Showing 51 changed files with 583 additions and 110 deletions.
40 changes: 40 additions & 0 deletions .changeset/gorgeous-schools-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
'@clerk/localizations': minor
'@clerk/clerk-js': minor
'@clerk/types': minor
---

Move passkey related apis to stable:

- Register passkey for a user
Usage: `await clerk.user.createPasskey()`
- Authenticate with passkey
Usage: `await clerk.client.signIn.authenticateWithPasskey()`
```ts
try {
await clerk.client.signIn.authenticateWithPasskey(...args);
}catch (e) {
if (isClerkRuntimeError(e)) {
if (err.code === 'passkey_operation_aborted') {
...
}
}
}
```
- ClerkRuntimeError codes introduced:
- `passkey_not_supported`
- `passkeys_pa_not_supported`
- `passkey_invalid_rpID_or_domain`
- `passkey_already_exists`
- `passkey_operation_aborted`
- `passkey_retrieval_cancelled`
- `passkey_retrieval_failed`
- `passkey_registration_cancelled`
- `passkey_registration_failed`

- Get the user's passkeys
`clerk.user.passkeys`
- Update the name of a passkey
`clerk.user.passkeys?.[0].update({name:'Company issued passkey'})`
- Delete a passkey
`clerk.user.passkeys?.[0].delete()`
4 changes: 2 additions & 2 deletions packages/clerk-js/src/core/resources/Passkey.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type {
__experimental_PublicKeyCredentialWithAuthenticatorAttestationResponse,
DeletedObjectJSON,
DeletedObjectResource,
PasskeyJSON,
PasskeyResource,
PasskeyVerificationResource,
PublicKeyCredentialWithAuthenticatorAttestationResponse,
UpdatePasskeyParams,
} from '@clerk/types';

Expand Down Expand Up @@ -42,7 +42,7 @@ export class Passkey extends BaseResource implements PasskeyResource {

private static async attemptVerification(
passkeyId: string,
credential: __experimental_PublicKeyCredentialWithAuthenticatorAttestationResponse,
credential: PublicKeyCredentialWithAuthenticatorAttestationResponse,
) {
const jsonPublicKeyCredential = serializePublicKeyCredential(credential);
return BaseResource._fetch({
Expand Down
22 changes: 12 additions & 10 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { deepSnakeToCamel, Poller } from '@clerk/shared';
import type {
__experimental_PasskeyFactor,
AttemptFirstFactorParams,
AttemptSecondFactorParams,
AuthenticateWithPasskeyParams,
AuthenticateWithRedirectParams,
AuthenticateWithWeb3Params,
CreateEmailLinkFlowReturn,
EmailCodeConfig,
EmailLinkConfig,
PassKeyConfig,
PasskeyFactor,
PhoneCodeConfig,
PrepareFirstFactorParams,
PrepareSecondFactorParams,
Expand Down Expand Up @@ -85,9 +87,7 @@ export class SignIn extends BaseResource implements SignInResource {
prepareFirstFactor = (factor: PrepareFirstFactorParams): Promise<SignInResource> => {
let config;
switch (factor.strategy) {
// @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
case 'passkey':
// @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
config = {} as PassKeyConfig;
break;
case 'email_link':
Expand Down Expand Up @@ -132,10 +132,8 @@ export class SignIn extends BaseResource implements SignInResource {
attemptFirstFactor = (attemptFactor: AttemptFirstFactorParams): Promise<SignInResource> => {
let config;
switch (attemptFactor.strategy) {
// @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
case 'passkey':
config = {
// @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
publicKeyCredential: JSON.stringify(serializePublicKeyCredentialAssertion(attemptFactor.publicKeyCredential)),
};
break;
Expand Down Expand Up @@ -263,9 +261,7 @@ export class SignIn extends BaseResource implements SignInResource {
});
};

public __experimental_authenticateWithPasskey = async (params?: {
flow?: 'autofill' | 'discoverable';
}): Promise<SignInResource> => {
public authenticateWithPasskey = async (params?: AuthenticateWithPasskeyParams): Promise<SignInResource> => {
const { flow } = params || {};

/**
Expand All @@ -286,7 +282,7 @@ export class SignIn extends BaseResource implements SignInResource {
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;
) as PasskeyFactor;

if (!passKeyFactor) {
clerkVerifyPasskeyCalledBeforeCreate();
Expand Down Expand Up @@ -324,11 +320,17 @@ export class SignIn extends BaseResource implements SignInResource {

return this.attemptFirstFactor({
publicKeyCredential,
// @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
strategy: 'passkey',
});
};

// TODO-PASSKEYS: Remove in the next minor
public __experimental_authenticateWithPasskey = async (params?: {
flow?: 'autofill' | 'discoverable';
}): Promise<SignInResource> => {
return this.authenticateWithPasskey(params);
};

validatePassword: ReturnType<typeof createValidatePassword> = (password, cb) => {
if (SignIn.clerk.__unstable__environment?.userSettings.passwordSettings) {
return createValidatePassword({
Expand Down
7 changes: 7 additions & 0 deletions packages/clerk-js/src/core/resources/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ export class User extends BaseResource implements UserResource {
phoneNumbers: PhoneNumberResource[] = [];
web3Wallets: Web3WalletResource[] = [];
externalAccounts: ExternalAccountResource[] = [];
// TODO-PASSKEY: Remove in the next minor
__experimental_passkeys: PasskeyResource[] = [];
passkeys: PasskeyResource[] = [];

samlAccounts: SamlAccountResource[] = [];

Expand Down Expand Up @@ -135,6 +137,10 @@ export class User extends BaseResource implements UserResource {
return Passkey.registerPasskey();
};

createPasskey = (): Promise<PasskeyResource> => {
return Passkey.registerPasskey();
};

createPhoneNumber = (params: CreatePhoneNumberParams): Promise<PhoneNumberResource> => {
const { phoneNumber } = params || {};
return new PhoneNumber(
Expand Down Expand Up @@ -339,6 +345,7 @@ export class User extends BaseResource implements UserResource {
);

this.__experimental_passkeys = (data.passkeys || []).map(passkey => new Passkey(passkey));
this.passkeys = (data.passkeys || []).map(passkey => new Passkey(passkey));

this.organizationMemberships = (data.organization_memberships || []).map(om => new OrganizationMembership(om));

Expand Down
4 changes: 2 additions & 2 deletions packages/clerk-js/src/core/resources/Verification.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { parseError } from '@clerk/shared/error';
import type {
__experimental_PublicKeyCredentialCreationOptionsWithoutExtensions,
ClerkAPIError,
PasskeyVerificationResource,
PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialCreationOptionsWithoutExtensions,
SignUpVerificationJSON,
SignUpVerificationResource,
SignUpVerificationsJSON,
Expand Down Expand Up @@ -58,7 +58,7 @@ export class Verification extends BaseResource implements VerificationResource {
}

export class PasskeyVerification extends Verification implements PasskeyVerificationResource {
publicKey: __experimental_PublicKeyCredentialCreationOptionsWithoutExtensions | null = null;
publicKey: PublicKeyCredentialCreationOptionsWithoutExtensions | null = null;

constructor(data: VerificationJSON | null) {
super(data);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -765,15 +765,15 @@ describe('SignInFactorOne', () => {
f.withPreferredSignInStrategy({ strategy: 'otp' });
f.startSignInWithEmailAddress({ supportPasskey: true, supportEmailCode: true });
});
fixtures.signIn.__experimental_authenticateWithPasskey.mockResolvedValue({
fixtures.signIn.authenticateWithPasskey.mockResolvedValue({
status: 'complete',
} as SignInResource);
const { userEvent } = render(<SignInFactorOne />, { wrapper });

await userEvent.click(screen.getByText('Continue'));

await waitFor(() => {
expect(fixtures.signIn.__experimental_authenticateWithPasskey).toHaveBeenCalled();
expect(fixtures.signIn.authenticateWithPasskey).toHaveBeenCalled();
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,14 @@ describe('SignInStart', () => {
});
});

fixtures.signIn.__experimental_authenticateWithPasskey.mockResolvedValue({
fixtures.signIn.authenticateWithPasskey.mockResolvedValue({
status: 'complete',
} as SignInResource);
render(<SignInStart />, { wrapper });
expect(screen.queryByText('Use passkey instead')).not.toBeInTheDocument();

await waitFor(() => {
expect(fixtures.signIn.__experimental_authenticateWithPasskey).toHaveBeenCalledWith({
expect(fixtures.signIn.authenticateWithPasskey).toHaveBeenCalledWith({
flow: 'autofill',
});
});
Expand Down Expand Up @@ -145,12 +145,12 @@ describe('SignInStart', () => {
show_sign_in_button: true,
});
});
fixtures.signIn.__experimental_authenticateWithPasskey.mockResolvedValue({
fixtures.signIn.authenticateWithPasskey.mockResolvedValue({
status: 'complete',
} as SignInResource);
const { userEvent } = render(<SignInStart />, { wrapper });
await userEvent.click(screen.getByText('Use passkey instead'));
expect(fixtures.signIn.__experimental_authenticateWithPasskey).toHaveBeenCalledWith({
expect(fixtures.signIn.authenticateWithPasskey).toHaveBeenCalledWith({
flow: 'discoverable',
});
});
Expand Down
6 changes: 3 additions & 3 deletions packages/clerk-js/src/ui/components/SignIn/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ function useHandleAuthenticateWithPasskey(onSecondFactor: () => Promise<unknown>
const { setActive } = useClerk();
const supportEmail = useSupportEmail();
const { navigateAfterSignIn } = useSignInContext();
const { __experimental_authenticateWithPasskey } = useCoreSignIn();
const { authenticateWithPasskey } = useCoreSignIn();

useEffect(() => {
return () => {
__internal_WebAuthnAbortService.abort();
};
}, []);

return useCallback(async (...args: Parameters<typeof __experimental_authenticateWithPasskey>) => {
return useCallback(async (...args: Parameters<typeof authenticateWithPasskey>) => {
try {
const res = await __experimental_authenticateWithPasskey(...args);
const res = await authenticateWithPasskey(...args);
switch (res.status) {
case 'complete':
return setActive({ session: res.createdSessionId, beforeEmit: navigateAfterSignIn });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ export const UpdatePasskeyForm = withCardStateProvider((props: UpdatePasskeyForm

return (
<FormContainer
headerTitle={localizationKeys('userProfile.__experimental_passkeyScreen.title__rename')}
headerSubtitle={localizationKeys('userProfile.__experimental_passkeyScreen.subtitle__rename')}
headerTitle={localizationKeys('userProfile.passkeyScreen.title__rename')}
headerSubtitle={localizationKeys('userProfile.passkeyScreen.subtitle__rename')}
>
<Form.Root onSubmit={addEmail}>
<Form.ControlRow elementId={passkeyNameField.id}>
Expand Down Expand Up @@ -96,12 +96,12 @@ export const PasskeySection = () => {

return (
<ProfileSection.Root
title={localizationKeys('userProfile.start.__experimental_passkeysSection.title')}
title={localizationKeys('userProfile.start.passkeysSection.title')}
centered={false}
id='passkeys'
>
<ProfileSection.ItemList id='passkeys'>
{user.__experimental_passkeys.map(passkey => (
{user.passkeys.map(passkey => (
<Action.Root key={passkey.id}>
<PasskeyItem
key={passkey.id}
Expand Down Expand Up @@ -173,11 +173,11 @@ const ActiveDeviceMenu = () => {

const actions = [
{
label: localizationKeys('userProfile.start.__experimental_passkeysSection.menuAction__rename'),
label: localizationKeys('userProfile.start.passkeysSection.menuAction__rename'),
onClick: () => open('rename'),
},
{
label: localizationKeys('userProfile.start.__experimental_passkeysSection.menuAction__destructive'),
label: localizationKeys('userProfile.start.passkeysSection.menuAction__destructive'),
isDestructive: true,
onClick: () => open('remove'),
},
Expand All @@ -194,7 +194,7 @@ const AddPasskeyButton = () => {

const handleCreatePasskey = async () => {
try {
await user?.__experimental_createPasskey();
await user?.createPasskey();
} catch (e) {
handleError(e, [], card.setError);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,8 @@ export const RemovePasskeyForm = (props: RemovePasskeyFormProps) => {

return (
<RemoveResourceForm
title={localizationKeys('userProfile.__experimental_passkeyScreen.removeResource.title')}
messageLine1={localizationKeys('userProfile.__experimental_passkeyScreen.removeResource.messageLine1', {
title={localizationKeys('userProfile.passkeyScreen.removeResource.title')}
messageLine1={localizationKeys('userProfile.passkeyScreen.removeResource.messageLine1', {
name: passkey.name,
})}
deleteResource={passkey.delete}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe('PasskeySection', () => {
it('create a new passkey', async () => {
const { wrapper, fixtures } = await createFixtures(withPasskeys);

fixtures.clerk.user?.__experimental_createPasskey.mockReturnValueOnce(Promise.resolve({} as any));
fixtures.clerk.user?.createPasskey.mockReturnValueOnce(Promise.resolve({} as any));
const { getByRole, userEvent } = render(
<CardStateProvider>
<PasskeySection />
Expand All @@ -72,7 +72,7 @@ describe('PasskeySection', () => {
);

await userEvent.click(getByRole('button', { name: 'Add a passkey' }));
expect(fixtures.clerk.user?.__experimental_createPasskey).toHaveBeenCalled();
expect(fixtures.clerk.user?.createPasskey).toHaveBeenCalled();
});
});

Expand Down Expand Up @@ -101,7 +101,7 @@ describe('PasskeySection', () => {
it('update the name of a new passkey', async () => {
const { wrapper, fixtures } = await createFixtures(withPasskeys);

fixtures.clerk.user?.__experimental_passkeys[0].update.mockResolvedValue({} as PasskeyResource);
fixtures.clerk.user?.passkeys[0].update.mockResolvedValue({} as PasskeyResource);
const { getByRole, userEvent, getByText, getByLabelText } = render(
<CardStateProvider>
<PasskeySection />
Expand All @@ -121,7 +121,7 @@ describe('PasskeySection', () => {
await userEvent.type(getByLabelText(/Name of Passkey/i), 'os');
expect(getByRole('button', { name: /save$/i })).not.toHaveAttribute('disabled');
await userEvent.click(getByRole('button', { name: /save$/i }));
expect(fixtures.clerk.user?.__experimental_passkeys[0].update).toHaveBeenCalledWith({ name: 'Chrome on Macos' });
expect(fixtures.clerk.user?.passkeys[0].update).toHaveBeenCalledWith({ name: 'Chrome on Macos' });
});
});

Expand Down Expand Up @@ -157,7 +157,7 @@ describe('PasskeySection', () => {
{ wrapper },
);

fixtures.clerk.user?.__experimental_passkeys[0].delete.mockResolvedValue({
fixtures.clerk.user?.passkeys[0].delete.mockResolvedValue({
object: 'passkey',
deleted: true,
});
Expand All @@ -173,7 +173,7 @@ describe('PasskeySection', () => {
await waitFor(() => getByRole('heading', { name: /remove passkey/i }));

await userEvent.click(getByRole('button', { name: /remove/i }));
expect(fixtures.clerk.user?.__experimental_passkeys[0].delete).toHaveBeenCalled();
expect(fixtures.clerk.user?.passkeys[0].delete).toHaveBeenCalled();

await waitFor(() => expect(queryByRole('heading', { name: /remove passkey/i })).not.toBeInTheDocument());
});
Expand Down
2 changes: 1 addition & 1 deletion packages/clerk-js/src/ui/utils/test/mockHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const mockClerkMethods = (clerk: LoadedClerk): DeepJestMocked<LoadedClerk
mockMethodsOf(m);
mockMethodsOf(m.organization);
});
session.user?.__experimental_passkeys.forEach(m => mockMethodsOf(m));
session.user?.passkeys.forEach(m => mockMethodsOf(m));
});
mockProp(clerk, 'navigate');
mockProp(clerk, 'setActive');
Expand Down
Loading

0 comments on commit 2352149

Please sign in to comment.