diff --git a/.changeset/tender-icons-bake.md b/.changeset/tender-icons-bake.md new file mode 100644 index 0000000000..ceb2e4a03c --- /dev/null +++ b/.changeset/tender-icons-bake.md @@ -0,0 +1,7 @@ +--- +'@clerk/clerk-js': minor +'@clerk/types': minor +--- + +Improve redirects on OAuth callback. Now, if you try to sign up with a provider that allows unverified accounts, it will +navigate to the appropriate change when needed, fixing the broken flow. diff --git a/packages/clerk-js/src/core/clerk.test.ts b/packages/clerk-js/src/core/clerk.test.ts index bdeeece80c..7a079f865c 100644 --- a/packages/clerk-js/src/core/clerk.test.ts +++ b/packages/clerk-js/src/core/clerk.test.ts @@ -593,7 +593,14 @@ describe('Clerk singleton', () => { }), ); - const mockSignUpCreate = jest.fn().mockReturnValue(Promise.resolve({ status: 'missing_requirements' })); + const mockSignUpCreate = jest.fn().mockReturnValue( + Promise.resolve( + new SignUp({ + status: 'missing_requirements', + missing_fields: ['phone_number'], + } as any as SignUpJSON), + ), + ); const sut = new Clerk(frontendApi); await sut.load({ @@ -700,6 +707,7 @@ describe('Clerk singleton', () => { signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', + missing_fields: [], verifications: { external_account: { status: 'unverified', @@ -1111,6 +1119,7 @@ describe('Clerk singleton', () => { signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', + missing_fields: ['last_name'], verifications: { external_account: { status: 'verified', @@ -1134,6 +1143,55 @@ describe('Clerk singleton', () => { expect(mockNavigate).toHaveBeenCalledWith('/sign-up#/continue'); }); }); + + it('redirects user to the verify-email-address url if the external account has unverified email and there are no missing requirements', async () => { + mockEnvironmentFetch.mockReturnValue( + Promise.resolve({ + authConfig: {}, + userSettings: mockUserSettings, + displayConfig: mockDisplayConfig, + isSingleSession: () => false, + isProduction: () => false, + isDevelopmentOrStaging: () => true, + }), + ); + + mockClientFetch.mockReturnValue( + Promise.resolve({ + activeSessions: [], + signIn: new SignIn(null), + signUp: new SignUp({ + status: 'missing_requirements', + missing_fields: [], + unverified_fields: ['email_address'], + verifications: { + email_address: { + status: 'unverified', + strategy: 'from_oauth_google', + next_action: 'needs_attempt', + }, + external_account: { + status: 'verified', + strategy: 'oauth_google', + external_verification_redirect_url: '', + error: null, + }, + }, + } as any as SignUpJSON), + }), + ); + + const sut = new Clerk(frontendApi); + await sut.load({ + navigate: mockNavigate, + }); + + await sut.handleRedirectCallback(); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith('/sign-up#/verify-email-address'); + }); + }); }); describe('.handleMagicLinkVerification()', () => { diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index fd3aecfcf6..c916e9f93d 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -44,6 +44,7 @@ import type { SignOut, SignOutCallback, SignOutOptions, + SignUpField, SignUpProps, SignUpResource, UnsubscribeCallback, @@ -53,6 +54,7 @@ import type { } from '@clerk/types'; import type { MountComponentRenderer } from '../ui/Components'; +import { completeSignUpFlow } from '../ui/components/SignUp/util'; import { appendAsQueryParams, buildURL, @@ -835,6 +837,7 @@ export default class Clerk implements ClerkInterface { const { externalAccount } = signUp.verifications; const su = { status: signUp.status, + missingFields: signUp.missingFields, externalAccountStatus: externalAccount.status, externalAccountErrorCode: externalAccount.error?.code, externalAccountSessionId: externalAccount.error?.meta?.sessionId, @@ -874,6 +877,23 @@ export default class Clerk implements ClerkInterface { buildURL({ base: displayConfig.signUpUrl, hashPath: '/continue' }, { stringify: true }), ); + const navigateToNextStepSignUp = ({ missingFields }: { missingFields: SignUpField[] }) => { + if (missingFields.length) { + return navigateToContinueSignUp(); + } + + return completeSignUpFlow({ + signUp, + verifyEmailPath: + params.verifyEmailAddressUrl || + buildURL({ base: displayConfig.signUpUrl, hashPath: '/verify-email-address' }, { stringify: true }), + verifyPhonePath: + params.verifyPhoneNumberUrl || + buildURL({ base: displayConfig.signUpUrl, hashPath: '/verify-phone-number' }, { stringify: true }), + navigate, + }); + }; + const userExistsButNeedsToSignIn = su.externalAccountStatus === 'transferable' && su.externalAccountErrorCode === 'external_account_exists'; @@ -903,7 +923,7 @@ export default class Clerk implements ClerkInterface { beforeEmit: navigateAfterSignUp, }); case 'missing_requirements': - return navigateToContinueSignUp(); + return navigateToNextStepSignUp({ missingFields: res.missingFields }); default: clerkOAuthCallbackDidNotCompleteSignInSignUp('sign in'); } @@ -939,7 +959,7 @@ export default class Clerk implements ClerkInterface { } if (su.externalAccountStatus === 'verified' && su.status === 'missing_requirements') { - return navigateToContinueSignUp(); + return navigateToNextStepSignUp({ missingFields: signUp.missingFields }); } return navigateToSignIn(); diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx index d85ff69995..2384a4056e 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx @@ -44,6 +44,8 @@ function SignUpRoutes(): JSX.Element { redirectUrl={signUpContext.redirectUrl} secondFactorUrl={signUpContext.secondFactorUrl} continueSignUpUrl='../continue' + verifyEmailAddressUrl='../verify-email-address' + verifyPhoneNumberUrl='../verify-phone-number' /> diff --git a/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpContinue.test.tsx b/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpContinue.test.tsx index 838bd5f830..80aef9c658 100644 --- a/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpContinue.test.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpContinue.test.tsx @@ -42,7 +42,9 @@ describe('SignUpContinue', () => { f.startSignUpWithEmailAddress(); }); render(, { wrapper }); - screen.getByText(/email address/i); + // Because the email address is already set, it should not be shown, + // as we're in PSU mode and it's not a missing field. + expect(screen.queryByText(/email address/i)).not.toBeInTheDocument(); screen.getByText(/password/i); }); diff --git a/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts b/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts index dd35b65e39..3fcf7a24da 100644 --- a/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts +++ b/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts @@ -50,12 +50,13 @@ export function determineActiveFields(fieldProps: FieldDeterminationProps): Fiel // If continuing with an existing sign-up, show only fields absolutely necessary to minimize fiction export function minimizeFieldsForExistingSignup(fields: Fields, signUp: SignUpResource) { if (signUp) { + const hasEmailFilled = !!signUp.emailAddress; const hasVerifiedEmail = signUp.verifications?.emailAddress?.status == 'verified'; const hasVerifiedPhone = signUp.verifications?.phoneNumber?.status == 'verified'; const hasVerifiedExternalAccount = signUp.verifications?.externalAccount?.status == 'verified'; const hasVerifiedWeb3Wallet = signUp.verifications?.web3Wallet?.status == 'verified'; - if (hasVerifiedEmail) { + if (hasEmailFilled || hasVerifiedEmail) { delete fields.emailAddress; } diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 155edd9c5a..59110b940f 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -451,6 +451,16 @@ export type HandleOAuthCallbackParams = { * Full URL or path to navigate after an incomplete sign up. */ continueSignUpUrl?: string | null; + + /** + * Full URL or path to navigate after requesting email verification. + */ + verifyEmailAddressUrl?: string | null; + + /** + * Full URL or path to navigate after requesting phone verification. + */ + verifyPhoneNumberUrl?: string | null; }; /**