Skip to content

Commit 7a4025b

Browse files
committed
fix: Align sign up if missing with oauth transfer navigation
1 parent c84148b commit 7a4025b

9 files changed

Lines changed: 394 additions & 61 deletions

File tree

packages/clerk-js/src/core/clerk.ts

Lines changed: 25 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ import type {
110110
SignOut,
111111
SignOutCallback,
112112
SignOutOptions,
113-
SignUpField,
114113
SignUpProps,
115114
SignUpRedirectOptions,
116115
SignUpResource,
@@ -138,7 +137,7 @@ import { ModuleManager } from '@/utils/moduleManager';
138137
import {
139138
ALLOWED_PROTOCOLS,
140139
buildURL,
141-
completeSignUpFlow,
140+
navigateToNextStepSignUp,
142141
createAllowedRedirectOrigins,
143142
createBeforeUnloadTracker,
144143
createPageLifecycle,
@@ -2270,39 +2269,15 @@ export class Clerk implements ClerkInterface {
22702269

22712270
const redirectUrls = new RedirectUrls(this.#options, params);
22722271

2273-
const navigateToContinueSignUp = makeNavigate(
2272+
const continueSignUpUrl =
22742273
params.continueSignUpUrl ||
2275-
buildURL(
2276-
{
2277-
base: displayConfig.signUpUrl,
2278-
hashPath: '/continue',
2279-
},
2280-
{ stringify: true },
2281-
),
2282-
);
2283-
2284-
const navigateToNextStepSignUp = ({ missingFields }: { missingFields: SignUpField[] }) => {
2285-
if (missingFields.length) {
2286-
return navigateToContinueSignUp();
2287-
}
2288-
2289-
return completeSignUpFlow({
2290-
signUp,
2291-
verifyEmailPath:
2292-
params.verifyEmailAddressUrl ||
2293-
buildURL(
2294-
{
2295-
base: displayConfig.signUpUrl,
2296-
hashPath: '/verify-email-address',
2297-
},
2298-
{ stringify: true },
2299-
),
2300-
verifyPhonePath:
2301-
params.verifyPhoneNumberUrl ||
2302-
buildURL({ base: displayConfig.signUpUrl, hashPath: '/verify-phone-number' }, { stringify: true }),
2303-
navigate,
2304-
});
2305-
};
2274+
buildURL({ base: displayConfig.signUpUrl, hashPath: '/continue' }, { stringify: true });
2275+
const verifyEmailAddressUrl =
2276+
params.verifyEmailAddressUrl ||
2277+
buildURL({ base: displayConfig.signUpUrl, hashPath: '/verify-email-address' }, { stringify: true });
2278+
const verifyPhoneNumberUrl =
2279+
params.verifyPhoneNumberUrl ||
2280+
buildURL({ base: displayConfig.signUpUrl, hashPath: '/verify-phone-number' }, { stringify: true });
23062281

23072282
const signInUrl = params.signInUrl || displayConfig.signInUrl;
23082283
const signUpUrl = params.signUpUrl || displayConfig.signUpUrl;
@@ -2401,7 +2376,14 @@ export class Clerk implements ClerkInterface {
24012376
},
24022377
});
24032378
case 'missing_requirements':
2404-
return navigateToNextStepSignUp({ missingFields: res.missingFields });
2379+
return navigateToNextStepSignUp({
2380+
signUp,
2381+
missingFields: res.missingFields,
2382+
continueSignUpUrl,
2383+
verifyEmailAddressUrl,
2384+
verifyPhoneNumberUrl,
2385+
navigate,
2386+
});
24052387
default:
24062388
clerkOAuthCallbackDidNotCompleteSignInSignUp('sign in');
24072389
}
@@ -2452,7 +2434,14 @@ export class Clerk implements ClerkInterface {
24522434
}
24532435

24542436
if (su.externalAccountStatus === 'verified' && su.status === 'missing_requirements') {
2455-
return navigateToNextStepSignUp({ missingFields: signUp.missingFields });
2437+
return navigateToNextStepSignUp({
2438+
signUp,
2439+
missingFields: signUp.missingFields,
2440+
continueSignUpUrl,
2441+
verifyEmailAddressUrl,
2442+
verifyPhoneNumberUrl,
2443+
navigate,
2444+
});
24562445
}
24572446

24582447
if (this.session?.currentTask) {

packages/clerk-js/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './beforeUnloadTracker';
22
export * from './billing';
33
export * from '@clerk/shared/internal/clerk-js/completeSignUpFlow';
4+
export * from '@clerk/shared/internal/clerk-js/navigateToNextStepSignUp';
45
export * from '@clerk/shared/internal/clerk-js/email';
56
export * from '@clerk/shared/internal/clerk-js/encoders';
67
export * from './errors';
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
3+
import type { SignUpField, SignUpResource } from '@/types';
4+
5+
import { navigateToNextStepSignUp } from '../navigateToNextStepSignUp';
6+
7+
const mockNavigate = vi.fn();
8+
9+
const URLS = {
10+
continueSignUpUrl: 'https://app.test/sign-up/continue',
11+
verifyEmailAddressUrl: 'https://app.test/sign-up/verify-email-address',
12+
verifyPhoneNumberUrl: 'https://app.test/sign-up/verify-phone-number',
13+
};
14+
15+
describe('navigateToNextStepSignUp', () => {
16+
beforeEach(() => {
17+
mockNavigate.mockReset();
18+
Object.defineProperty(window, 'location', {
19+
value: { search: '' },
20+
writable: true,
21+
});
22+
});
23+
24+
it('navigates to the continue page when there are missing fields', async () => {
25+
const signUp = {
26+
status: 'missing_requirements',
27+
missingFields: ['first_name'] as SignUpField[],
28+
unverifiedFields: [],
29+
} as unknown as SignUpResource;
30+
31+
await navigateToNextStepSignUp({
32+
signUp,
33+
missingFields: signUp.missingFields,
34+
...URLS,
35+
navigate: mockNavigate,
36+
});
37+
38+
expect(mockNavigate).toHaveBeenCalledTimes(1);
39+
expect(mockNavigate).toHaveBeenCalledWith(URLS.continueSignUpUrl);
40+
});
41+
42+
it('navigates to verify-email-address when email is unverified and there are no missing fields', async () => {
43+
const signUp = {
44+
status: 'missing_requirements',
45+
missingFields: [] as SignUpField[],
46+
unverifiedFields: ['email_address'],
47+
} as unknown as SignUpResource;
48+
49+
await navigateToNextStepSignUp({
50+
signUp,
51+
missingFields: signUp.missingFields,
52+
...URLS,
53+
navigate: mockNavigate,
54+
});
55+
56+
expect(mockNavigate).toHaveBeenCalledTimes(1);
57+
expect(mockNavigate).toHaveBeenCalledWith(URLS.verifyEmailAddressUrl, { searchParams: new URLSearchParams() });
58+
});
59+
60+
it('navigates to verify-phone-number when phone is unverified and there are no missing fields', async () => {
61+
const signUp = {
62+
status: 'missing_requirements',
63+
missingFields: [] as SignUpField[],
64+
unverifiedFields: ['phone_number'],
65+
} as unknown as SignUpResource;
66+
67+
await navigateToNextStepSignUp({
68+
signUp,
69+
missingFields: signUp.missingFields,
70+
...URLS,
71+
navigate: mockNavigate,
72+
});
73+
74+
expect(mockNavigate).toHaveBeenCalledTimes(1);
75+
expect(mockNavigate).toHaveBeenCalledWith(URLS.verifyPhoneNumberUrl, { searchParams: new URLSearchParams() });
76+
});
77+
78+
it('prefers email verification over phone verification when both are unverified', async () => {
79+
const signUp = {
80+
status: 'missing_requirements',
81+
missingFields: [] as SignUpField[],
82+
unverifiedFields: ['email_address', 'phone_number'],
83+
} as unknown as SignUpResource;
84+
85+
await navigateToNextStepSignUp({
86+
signUp,
87+
missingFields: signUp.missingFields,
88+
...URLS,
89+
navigate: mockNavigate,
90+
});
91+
92+
expect(mockNavigate).toHaveBeenCalledTimes(1);
93+
expect(mockNavigate).toHaveBeenCalledWith(URLS.verifyEmailAddressUrl, { searchParams: new URLSearchParams() });
94+
});
95+
96+
it('prefers the continue page when there are both missing fields and unverified fields', async () => {
97+
const signUp = {
98+
status: 'missing_requirements',
99+
missingFields: ['first_name'] as SignUpField[],
100+
unverifiedFields: ['email_address'],
101+
} as unknown as SignUpResource;
102+
103+
await navigateToNextStepSignUp({
104+
signUp,
105+
missingFields: signUp.missingFields,
106+
...URLS,
107+
navigate: mockNavigate,
108+
});
109+
110+
expect(mockNavigate).toHaveBeenCalledTimes(1);
111+
expect(mockNavigate).toHaveBeenCalledWith(URLS.continueSignUpUrl);
112+
});
113+
114+
it('does nothing when sign-up has no missing fields and no unverified fields', async () => {
115+
const signUp = {
116+
status: 'missing_requirements',
117+
missingFields: [] as SignUpField[],
118+
unverifiedFields: [],
119+
} as unknown as SignUpResource;
120+
121+
await navigateToNextStepSignUp({
122+
signUp,
123+
missingFields: signUp.missingFields,
124+
...URLS,
125+
navigate: mockNavigate,
126+
});
127+
128+
expect(mockNavigate).not.toHaveBeenCalled();
129+
});
130+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { SignUpField, SignUpResource } from '../../types';
2+
import { completeSignUpFlow } from './completeSignUpFlow';
3+
4+
type NavigateToNextStepSignUpProps = {
5+
signUp: SignUpResource;
6+
missingFields: SignUpField[];
7+
continueSignUpUrl: string;
8+
verifyEmailAddressUrl: string;
9+
verifyPhoneNumberUrl: string;
10+
navigate: (to: string, options?: { searchParams?: URLSearchParams }) => Promise<unknown>;
11+
};
12+
13+
/**
14+
* Routes a sign-up that's still in `missing_requirements` to the appropriate
15+
* next step:
16+
*
17+
* - If there are missing fields, go straight to the continue page so the user
18+
* can fill them in.
19+
* - Otherwise, hand off to `completeSignUpFlow` which routes unverified email
20+
* or phone identifications to their respective verify pages.
21+
*
22+
* Used by both the OAuth callback handler and the sign-in `signUpIfMissing`
23+
* transfer flow so they stay in lockstep.
24+
*
25+
* @internal
26+
*/
27+
export const navigateToNextStepSignUp = ({
28+
signUp,
29+
missingFields,
30+
continueSignUpUrl,
31+
verifyEmailAddressUrl,
32+
verifyPhoneNumberUrl,
33+
navigate,
34+
}: NavigateToNextStepSignUpProps): Promise<unknown> | undefined => {
35+
if (missingFields.length) {
36+
return navigate(continueSignUpUrl);
37+
}
38+
39+
return completeSignUpFlow({
40+
signUp,
41+
verifyEmailPath: verifyEmailAddressUrl,
42+
verifyPhonePath: verifyPhoneNumberUrl,
43+
navigate,
44+
});
45+
};

packages/ui/src/components/SignIn/SignInFactorOneCodeForm.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const SignInFactorOneCodeForm = (props: SignInFactorOneCodeFormProps) =>
3737
const card = useCardState();
3838
const { navigate } = useRouter();
3939
const ctx = useSignInContext();
40-
const { afterSignInUrl, afterSignUpUrl, navigateOnSetActive, isCombinedFlow } = ctx;
40+
const { afterSignInUrl, afterSignUpUrl, signUpUrl, isCombinedFlow } = ctx;
4141
const { setActive } = useClerk();
4242
const { userSettings } = useEnvironment();
4343
const supportEmail = useSupportEmail();
@@ -128,7 +128,7 @@ export const SignInFactorOneCodeForm = (props: SignInFactorOneCodeFormProps) =>
128128
clerk,
129129
navigate,
130130
afterSignUpUrl,
131-
navigateOnSetActive,
131+
signUpUrl,
132132
unsafeMetadata: ctx.unsafeMetadata,
133133
}).catch(reject);
134134
}

packages/ui/src/components/SignIn/SignInFactorOneEmailLinkCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const SignInFactorOneEmailLinkCard = (props: SignInFactorOneEmailLinkCard
2727
const card = useCardState();
2828
const signIn = useCoreSignIn();
2929
const signInContext = useSignInContext();
30-
const { signInUrl, afterSignInUrl, afterSignUpUrl, isCombinedFlow, navigateOnSetActive } = signInContext;
30+
const { signInUrl, signUpUrl, afterSignInUrl, afterSignUpUrl, isCombinedFlow } = signInContext;
3131
const { navigate } = useRouter();
3232
const { setActive } = useClerk();
3333
const { userSettings } = useEnvironment();
@@ -73,7 +73,7 @@ export const SignInFactorOneEmailLinkCard = (props: SignInFactorOneEmailLinkCard
7373
clerk,
7474
navigate,
7575
afterSignUpUrl,
76-
navigateOnSetActive,
76+
signUpUrl,
7777
unsafeMetadata: signInContext.unsafeMetadata,
7878
});
7979
} else if (ver.verifiedFromTheSameClient()) {

0 commit comments

Comments
 (0)