Skip to content

feat(elements): Add support for sign in with passkey#3472

Merged
panteliselef merged 24 commits intomainfrom
elef/passkey-elements
Jun 11, 2024
Merged

feat(elements): Add support for sign in with passkey#3472
panteliselef merged 24 commits intomainfrom
elef/passkey-elements

Conversation

@panteliselef
Copy link
Copy Markdown
Contributor

@panteliselef panteliselef commented May 30, 2024

Description

This PR add support for passkey usage within a SignIn flow

APIs introduced:

  • <SignIn.Passkey />
  • <SignIn.SupportedStrategy name='passkey'>
  • <SignIn.Strategy name='passkey'>
  • <Clerk.Input type='text' passkeyAutofill>

Usage Examples:

  • <SignIn.Passkey />
<SignIn.Step name='start'>
  <SignIn.Passkey>
    <Clerk.Loading>
      {isLoading => (isLoading ? <Spinner /> : 'Use passkey instead')}. 
    </Clerk.Loading>
  </SignIn.Passkey>
</SignIn.Step>
  • <SignIn.SupportedStrategy name='passkey'>
<SignIn.SupportedStrategy asChild name='passkey' >
  <Button>use passkey</Button>
</SignIn.SupportedStrategy>
  • <SignIn.Strategy name='passkey'>
<SignIn.Strategy name='passkey'>
  <p className='text-sm'>
    Welcome back <SignIn.Salutation />!
  </p>

  <CustomSubmit>Continue with Passkey</CustomSubmit>
</SignIn.Strategy>
  • <Clerk.Input type='text' passkeyAutofill>
<SignIn.Step name='start'>
  <Clerk.Field name='identifier'>
    <Clerk.Label className='sr-only'>Email</Clerk.Label>
    <Clerk.Input passkeyAutofill placeholder='Enter your email address' />
    <Clerk.FieldError />
  </Clerk.Field>
</SignIn.Step/>

Docs PR: clerk/clerk-docs#1132

Checklist

  • npm test runs as expected.
  • npm run build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 30, 2024

🦋 Changeset detected

Latest commit: d16809f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
@clerk/elements Minor
@clerk/clerk-js Minor
@clerk/shared Minor
@clerk/chrome-extension Patch
@clerk/clerk-expo Patch
@clerk/backend Patch
@clerk/express Patch
@clerk/fastify Patch
@clerk/nextjs Patch
@clerk/clerk-react Patch
@clerk/remix Patch
@clerk/clerk-sdk-node Patch
@clerk/testing Patch
@clerk/ui Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Comment thread packages/elements/src/internals/machines/sign-in/utils/starting-factors.ts Outdated
Comment thread packages/elements/src/internals/machines/sign-in/verification.machine.ts Outdated
Comment on lines +372 to +381
case 'passkey': {
return await parent.getSnapshot().context.clerk.client.signIn.authenticateWithPasskey();
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When "Submit" is hit we don't want to call attempt, instead call authenticateWithPasskey

Comment on lines +276 to +308
const [isSupported, setIsSupported] = React.useState(false);
React.useEffect(() => {
async function runAutofillPasskey() {
const _isSupported = await isWebAuthnAutofillSupported().catch(() => false);
setIsSupported(_isSupported);
}

// @ts-expect-error - Depending on type the props can be different
if (passthroughProps?.passkeyAutofill) {
runAutofillPasskey();
}

// @ts-expect-error - Depending on type the props can be different
}, [passthroughProps?.passkeyAutofill]);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be handled inside an actor ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which part are you referring to?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, passkey specific logic shouldn't live in the common input element like this. Let's try to move it to an actor.

Why does passkeyAutofill need to be on the input?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only necessary if we want to support passkey autofill.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we rely on "the platform" here? It doesn't feel necessary to create a separate abstraction if all it takes is specifying a native property on the element, unless we need to execute some specific logic to enable autofill.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need to call authenticateWithPasskey internally

Comment thread packages/elements/src/react/common/form/index.tsx Outdated
@panteliselef panteliselef changed the title Elef/passkey elements feat(elements): Add support for sign in with passkey May 30, 2024
Comment on lines +138 to +149
on: {
'AUTHENTICATE.PASSKEY': {
guard: not('isExampleMode'),
target: 'AttemptingPasskey',
reenter: true,
},
SUBMIT: {
guard: not('isExampleMode'),
target: 'Attempting',
reenter: true,
},
},
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enabling "autofill" is kind of fire and forget action, so we should be able to recover from this state and not get stuck. Promise will await until user interact with the field and select a passkey and that may never happen.

Comment thread packages/elements/src/internals/machines/sign-in/router.machine.ts Outdated
Comment thread packages/elements/src/internals/machines/sign-in/router.machine.ts Outdated
@panteliselef panteliselef self-assigned this May 31, 2024
@panteliselef panteliselef marked this pull request as ready for review May 31, 2024 09:48
Comment thread packages/elements/src/internals/machines/sign-in/start.machine.ts Outdated
Comment thread packages/elements/src/internals/machines/sign-in/router.types.ts Outdated
Comment thread packages/elements/src/internals/machines/sign-in/router.machine.ts Outdated
Comment thread packages/elements/src/internals/machines/sign-in/router.machine.ts Outdated
Comment thread packages/elements/src/internals/machines/sign-in/start.types.ts Outdated
Comment thread .changeset/fuzzy-bees-doubt.md Outdated
Comment thread packages/elements/src/react/common/form/index.tsx Outdated
Comment thread packages/elements/src/react/common/form/index.tsx Outdated
Comment thread packages/elements/src/react/sign-in/action/passkey.tsx
Copy link
Copy Markdown
Member

@brkalow brkalow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good. I'd like to see the passkey specific logic moved out of the input logic.

Comment on lines +276 to +308
const [isSupported, setIsSupported] = React.useState(false);
React.useEffect(() => {
async function runAutofillPasskey() {
const _isSupported = await isWebAuthnAutofillSupported().catch(() => false);
setIsSupported(_isSupported);
}

// @ts-expect-error - Depending on type the props can be different
if (passthroughProps?.passkeyAutofill) {
runAutofillPasskey();
}

// @ts-expect-error - Depending on type the props can be different
}, [passthroughProps?.passkeyAutofill]);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, passkey specific logic shouldn't live in the common input element like this. Let's try to move it to an actor.

Why does passkeyAutofill need to be on the input?

Comment thread packages/elements/src/internals/machines/sign-in/verification.machine.ts Outdated
Comment thread .changeset/fuzzy-bees-doubt.md Outdated
Comment thread packages/elements/src/react/common/form/index.tsx Outdated
Comment thread packages/elements/src/react/sign-in/action/action.tsx Outdated
Comment thread packages/elements/src/react/sign-in/passkey.tsx Outdated
Copy link
Copy Markdown
Member

@brkalow brkalow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good. Let's move the logic from the component into useInput() to keep the component layer thin.

Any ideas for not referencing the sign in flow actor directly in the common input components?

Comment on lines +612 to +616
React.useEffect(() => {
if (passkeyAutofillSupported) {
signInRouterRef?.send({ type: 'AUTHENTICATE.PASSKEY.AUTOFILL' });
}
}, [passkeyAutofillSupported, signInRouterRef]);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be in the useInput() hook? Ideally the components here are just thin wrappers around the hook, which contains the business logic

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably yes, but if felt like something only this component should care about.

because we are using refs here and not the context, it will not error, but i thought it would ok to not litter useInput with sign in specific logic.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out with the current setup this is a bit hard. useSignInPasskeyAutofill will use the useSelector underneath which would error when the SignInRouter context is not available.

(props: FormInputProps, forwardedRef) => {
const clerk = useClerk();
const passkeyAutofillProp = (props as PasskeyInputProps).passkeyAutofill;
const signInRouterRef = SignInRouterCtx.useActorRef(true);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not crazy about referencing the sign in actor directly in these components, but not sure if there's another way. 🤔

Maybe @tmilewski has an idea?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this better ?

<SignIn.Passkey>
   <Clerk.Input /> 
</SignIn.Passkey>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see issues with that as well, because SignIn.Passkey wouldn't know whether it needs to pass the autofill prop or not. To avoid leaking the prop to the dom for everything else, but only pass it to Clerk.Input.

Another thought is to have a SignIn.PasskeyInput, but this does not feel right.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are now simply detecting autoComplete="webauthn"

const field = useInput(props);

const hasPasskeyAutofillProp = Boolean(field.props.autoComplete?.includes('webauthn'));
const allowedTypeForPasskey = (['text', 'email'] as FormInputProps['type'][]).includes(field.props.type);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we consider tel here for phone numbers?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are correct

Copy link
Copy Markdown
Member

@brkalow brkalow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@panteliselef panteliselef enabled auto-merge (squash) June 11, 2024 08:02
@panteliselef panteliselef merged commit 4ec3f63 into main Jun 11, 2024
@panteliselef panteliselef deleted the elef/passkey-elements branch June 11, 2024 08:16
This was referenced Jun 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants