Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/rich-badgers-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/elements': minor
---

Introduce support for `<SignIn.Captcha />` and `<SignIn.Step name='sso-callback'>`. This allows rendering of a CAPTCHA widget when a sign in attempt is transferred to a sign up attempt.
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,7 @@ export default function SignInPage() {
<CustomSubmit>Update Password</CustomSubmit>
</div>
</SignIn.Step>
<SignIn.Step name='sso-callback'></SignIn.Step>
</div>
</SignIn.Root>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,6 @@ export const handleRedirectCallback = fromCallback<AnyEventObject, HandleRedirec

void loadedClerk.handleRedirectCallback(
{
signInForceRedirectUrl: ClerkJSNavigationEvent.complete,
signInFallbackRedirectUrl: ClerkJSNavigationEvent.complete,
signUpForceRedirectUrl: ClerkJSNavigationEvent.complete,
signUpFallbackRedirectUrl: ClerkJSNavigationEvent.complete,
Comment on lines -81 to -84

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Now that we're not passing beforeEmit to setActive internally, our custom navigate hackery wasn't working as intended here. Removing the URLs allows navigate to complete.

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.

Does this mean we broke the usage of handleRedirectCallback for everyone ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@panteliselef for elements, yes 😅

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey, trying to use <SignIn.Captcha /> but I see Property 'Captcha' does not exist ....

I believe you forgot to export this component in index.ts right?

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.

@corentinmusard yes, good catch. PR here #4548

continueSignUpUrl: ClerkJSNavigationEvent.continue,
firstFactorUrl: ClerkJSNavigationEvent.signIn,
resetPasswordUrl: ClerkJSNavigationEvent.resetPassword,
Expand Down
70 changes: 70 additions & 0 deletions packages/elements/src/react/sign-in/captcha.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Slot } from '@radix-ui/react-slot';
import * as React from 'react';

import { CAPTCHA_ELEMENT_ID } from '~/internals/constants';
import { ClerkElementsRuntimeError } from '~/internals/errors';

import { useActiveTags } from '../hooks';
import { SignInRouterCtx } from './context';

export type SignInCaptchaElement = React.ElementRef<'div'>;

type CaptchaElementProps = Omit<
React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>,
'id' | 'children'
>;

export type SignInCaptchaProps =
| ({
asChild: true;
/* Must only be a self-closing element/component */
children: React.ReactElement;
} & CaptchaElementProps)
| ({ asChild?: false; children?: undefined } & CaptchaElementProps);

/**
* The `<SignIn.Captcha>` component is used to render the Cloudflare Turnstile widget. It must be used within the `<SignIn.Step name="sso-callback">` component.
*
* If utilizing the `asChild` prop, the component must be a self-closing element or component. Any children passed to the immediate child component of <SignIn.Captcha> will be ignored.
*
* @param {boolean} [asChild] - If true, `<Captcha />` will render as its child element, passing along any necessary props.
*
* @example
* <SignIn.Root>
* <SignIn.Step name="sso-callback">
* <SignIn.Captcha />
* </SignIn.Step>
* </SignIn.Root>
*
* @example
* <SignIn.Root>
* <SignIn.Step name="sso-callback">
* <SignIn.Captcha asChild>
* <aside/>
* </SignIn.Captcha>
* </SignIn.Step>
* </SignIn.Root>
*/

export const SignInCaptcha = React.forwardRef<SignInCaptchaElement, SignInCaptchaProps>(
({ asChild, children, ...rest }, forwardedRef) => {
const routerRef = SignInRouterCtx.useActorRef();
const activeState = useActiveTags(routerRef, 'step:callback');

if (!activeState) {
throw new ClerkElementsRuntimeError(
'<Captcha> must be used within the <SignIn.Step name="sso-callback"> component.',
);
}

const Comp = asChild ? Slot : 'div';

return (
<Comp
id={CAPTCHA_ELEMENT_ID}
{...rest}
ref={forwardedRef}
/>
);
},
);
13 changes: 13 additions & 0 deletions packages/elements/src/react/sign-in/sso-callback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { PropsWithChildren } from 'react';

import { useActiveTags } from '~/react/hooks';
import { SignInRouterCtx } from '~/react/sign-in/context';

export type SignInSSOCallbackProps = PropsWithChildren;

export function SignInSSOCallback({ children }: SignInSSOCallbackProps) {
const routerRef = SignInRouterCtx.useActorRef();
const activeState = useActiveTags(routerRef, 'step:callback');

return activeState ? children : null;
}
8 changes: 7 additions & 1 deletion packages/elements/src/react/sign-in/step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { ClerkElementsRuntimeError } from '~/internals/errors';
import { SignInChooseSession, type SignInChooseSessionProps } from './choose-session';
import { SignInChooseStrategy, type SignInChooseStrategyProps, SignInForgotPassword } from './choose-strategy';
import { SignInResetPassword, type SignInResetPasswordProps } from './reset-password';
import type { SignInSSOCallbackProps } from './sso-callback';
import { SignInSSOCallback } from './sso-callback';
import { SignInStart, type SignInStartProps } from './start';
import { SignInVerifications, type SignInVerificationsProps } from './verifications';

Expand All @@ -16,6 +18,7 @@ export const SIGN_IN_STEPS = {
'choose-session': 'choose-session',
'forgot-password': 'forgot-password',
'reset-password': 'reset-password',
'sso-callback': 'sso-callback',
} as const;

export type TSignInStep = (typeof SIGN_IN_STEPS)[keyof typeof SIGN_IN_STEPS];
Expand All @@ -26,7 +29,8 @@ export type SignInStepProps =
| StepWithProps<'verifications', SignInVerificationsProps>
| StepWithProps<'choose-strategy' | 'forgot-password', SignInChooseStrategyProps>
| StepWithProps<'reset-password', SignInResetPasswordProps>
| StepWithProps<'choose-session', SignInChooseSessionProps>;
| StepWithProps<'choose-session', SignInChooseSessionProps>
| StepWithProps<'sso-callback', SignInSSOCallbackProps>;

/**
* Render different steps of the sign-in flow. Initially the `'start'` step is rendered. Once a sign-in attempt has been created, `'verifications'` will be displayed. If during that verification step the user decides to choose a different method of signing in or verifying, the `'choose-strategy'` step will be displayed.
Expand Down Expand Up @@ -63,6 +67,8 @@ export function SignInStep(props: SignInStepProps) {
return <SignInResetPassword {...props} />;
case SIGN_IN_STEPS['choose-session']:
return <SignInChooseSession {...props} />;
case SIGN_IN_STEPS['sso-callback']:
return <SignInSSOCallback {...props} />;
default:
throw new ClerkElementsRuntimeError(`Invalid step name. Use: ${Object.keys(SIGN_IN_STEPS).join(',')}.`);
}
Expand Down