-
Notifications
You must be signed in to change notification settings - Fork 431
feat(nextjs,react): Add HandleSSOCallback component #7678
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
801a764
9cf0701
23104dc
f04428f
f056631
146a6c8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| '@clerk/chrome-extension': minor | ||
| '@clerk/nextjs': minor | ||
| '@clerk/react': minor | ||
| --- | ||
|
|
||
| Add `HandleSSOCallback` component which handles the SSO callback during custom flows, including support for sign-in-or-up. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| import type { SetActiveNavigate } from '@clerk/shared/types'; | ||
| import React, { type ReactNode, useEffect, useRef } from 'react'; | ||
|
|
||
| import { useClerk, useSignIn, useSignUp } from '../hooks'; | ||
|
|
||
| export interface HandleSSOCallbackProps { | ||
| /** | ||
| * Called when the SSO callback is complete and a session has been created. | ||
| */ | ||
| navigateToApp: (...params: Parameters<SetActiveNavigate>) => void; | ||
| /** | ||
| * Called when a sign-in requires additional verification, or a sign-up is transfered to a sign-in that requires | ||
| * additional verification. | ||
| */ | ||
| navigateToSignIn: () => void; | ||
| /** | ||
| * Called when a sign-in is transfered to a sign-up that requires additional verification. | ||
| */ | ||
| navigateToSignUp: () => void; | ||
| /** | ||
| * Can be provided to render a custom component while the SSO callback is being processed. This component should, at | ||
| * a minimum, render a `<div id='clerk-captcha'></div>` element to handle captchas. | ||
| */ | ||
| render?: () => ReactNode; | ||
| } | ||
|
|
||
| /** | ||
| * Use this component when building custom UI to handle the SSO callback and navigate to the appropriate page based on | ||
| * the status of the sign-in or sign-up. By default, this component might render a captcha element to handle captchas | ||
| * when required by the Clerk API. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * import { HandleSSOCallback } from '@clerk/react'; | ||
| * import { useNavigate } from 'react-router'; | ||
| * | ||
| * export default function Page() { | ||
| * const navigate = useNavigate(); | ||
| * | ||
| * return ( | ||
| * <HandleSSOCallback | ||
| * navigateToApp={({ session, decorateUrl }) => { | ||
| * if (session?.currentTask) { | ||
| * const destination = decorateUrl(`/onboarding/${session?.currentTask.key}`); | ||
| * if (destination.startsWith('http')) { | ||
| * window.location.href = destination; | ||
| * return; | ||
| * } | ||
| * navigate(destination); | ||
| * return; | ||
| * } | ||
| * | ||
| * const destination = decorateUrl('/dashboard'); | ||
| * if (destination.startsWith('http')) { | ||
| * window.location.href = destination; | ||
| * return; | ||
| * } | ||
| * navigate(destination); | ||
| * }} | ||
| * navigateToSignIn={() => { | ||
| * navigate('/sign-in'); | ||
| * }} | ||
| * navigateToSignUp={() => { | ||
| * navigate('/sign-up'); | ||
| * }} | ||
| * /> | ||
| * ); | ||
| * } | ||
| * ``` | ||
| */ | ||
| export function HandleSSOCallback(props: HandleSSOCallbackProps): ReactNode { | ||
| const { navigateToApp, navigateToSignIn, navigateToSignUp, render } = props; | ||
| const clerk = useClerk(); | ||
| const { signIn } = useSignIn(); | ||
| const { signUp } = useSignUp(); | ||
| const hasRun = useRef(false); | ||
|
|
||
| useEffect(() => { | ||
| (async () => { | ||
| if (!clerk.loaded || hasRun.current) { | ||
| return; | ||
| } | ||
| // Prevent re-running this effect if the page is re-rendered during session activation (such as on Next.js). | ||
| hasRun.current = true; | ||
|
|
||
| // If this was a sign-in, and it's complete, there's nothing else to do. | ||
| // Note: We perform a cast | ||
| if ((signIn.status as string) === 'complete') { | ||
| await signIn.finalize({ | ||
| navigate: async (...params) => { | ||
| navigateToApp(...params); | ||
| }, | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| // If the sign-up used an existing account, transfer it to a sign-in. | ||
| if (signUp.isTransferable) { | ||
| await signIn.create({ transfer: true }); | ||
| if (signIn.status === 'complete') { | ||
| await signIn.finalize({ | ||
| navigate: async (...params) => { | ||
| navigateToApp(...params); | ||
| }, | ||
| }); | ||
| return; | ||
| } | ||
| // The sign-in requires additional verification, so we need to navigate to the sign-in page. | ||
| return navigateToSignIn(); | ||
| } | ||
|
|
||
| if ( | ||
| signIn.status === 'needs_first_factor' && | ||
| !signIn.supportedFirstFactors?.every(f => f.strategy === 'enterprise_sso') | ||
| ) { | ||
| // The sign-in requires the use of a configured first factor, so navigate to the sign-in page. | ||
| return navigateToSignIn(); | ||
| } | ||
|
|
||
| // If the sign-in used an external account not associated with an existing user, create a sign-up. | ||
| if (signIn.isTransferable) { | ||
| await signUp.create({ transfer: true }); | ||
| if (signUp.status === 'complete') { | ||
| await signUp.finalize({ | ||
| navigate: async (...params) => { | ||
| navigateToApp(...params); | ||
| }, | ||
| }); | ||
| return; | ||
| } | ||
| return navigateToSignUp(); | ||
| } | ||
|
|
||
| if (signUp.status === 'complete') { | ||
| await signUp.finalize({ | ||
| navigate: async (...params) => { | ||
| navigateToApp(...params); | ||
| }, | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| if (signIn.status === 'needs_second_factor' || signIn.status === 'needs_new_password') { | ||
| // The sign-in requires a MFA token or a new password, so navigate to the sign-in page. | ||
| return navigateToSignIn(); | ||
| } | ||
|
|
||
| // The external account used to sign-in or sign-up was already associated with an existing user and active | ||
| // session on this client, so activate the session and navigate to the application. | ||
| if (signIn.existingSession || signUp.existingSession) { | ||
| const sessionId = signIn.existingSession?.sessionId || signUp.existingSession?.sessionId; | ||
| if (sessionId) { | ||
| // Because we're activating a session that's not the result of a sign-in or sign-up, we need to use the | ||
| // Clerk `setActive` API instead of the `finalize` API. | ||
| await clerk.setActive({ | ||
| session: sessionId, | ||
| navigate: async (...params) => { | ||
| return navigateToApp(...params); | ||
| }, | ||
| }); | ||
| return; | ||
| } | ||
| } | ||
| })(); | ||
| }, [clerk, signIn, signUp]); | ||
|
|
||
| if (render) { | ||
| return render(); | ||
| } | ||
|
|
||
| return ( | ||
| <div> | ||
| {/* Because a sign-in transferred to a sign-up might require captcha verification, make sure to render the | ||
| captcha element. */} | ||
|
Comment on lines
+173
to
+174
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we allow children for them to render a spinner here possibly?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I went with the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. to date we've used children, so I might lean towards children for now. We've yet to introduce a render prop but possibly in the future.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. stepping back do we even need either? This renders a div and the captcha (possibly). wouldn't folks probably write code that looks like this? In this situation I don't think they'd want to put their Spinner as a child of |
||
| <div id='clerk-captcha' /> | ||
| </div> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, let's look at the full HandleSSOCallback.tsx file cat -n packages/react/src/components/HandleSSOCallback.tsxRepository: clerk/javascript
Length of output: 7284
🏁 Script executed:
Repository: clerk/javascript
Length of output: 144
🏁 Script executed:
Repository: clerk/javascript
Length of output: 139
🏁 Script executed:
Repository: clerk/javascript
Length of output: 42
🏁 Script executed:
Repository: clerk/javascript
Length of output: 4287
🏁 Script executed:
Repository: clerk/javascript
Length of output: 44
🏁 Script executed:
Repository: clerk/javascript
Length of output: 42
🏁 Script executed:
Repository: clerk/javascript
Length of output: 42
🏁 Script executed:
Repository: clerk/javascript
Length of output: 1543
🏁 Script executed:
Repository: clerk/javascript
Length of output: 42
🏁 Script executed:
Repository: clerk/javascript
Length of output: 273
🏁 Script executed:
Repository: clerk/javascript
Length of output: 42
🏁 Script executed:
# List all files in the components __tests__ directory to see what tests exist ls -1 packages/react/src/components/__tests__/Repository: clerk/javascript
Length of output: 398
🏁 Script executed:
Repository: clerk/javascript
Length of output: 562
🏁 Script executed:
Repository: clerk/javascript
Length of output: 146
🏁 Script executed:
Repository: clerk/javascript
Length of output: 42
🏁 Script executed:
Repository: clerk/javascript
Length of output: 93
🏁 Script executed:
Repository: clerk/javascript
Length of output: 187
Add tests to cover the new HandleSSOCallback component.
The component is new with no test coverage. Per project guidelines, tests must be added to cover changes.
The useEffect dependency array triggers an
exhaustive-depswarning (warn-level) for the missing callback props, but thehasRunguard prevents re-execution regardless. If desired, add them to dependencies to silence the warning, but the pattern is valid as-is.🤖 Prompt for AI Agents