Skip to content

Commit de18497

Browse files
authored
fix(clerk-js): Disambiguate redirect url options between flows (#5645)
1 parent 03284da commit de18497

File tree

8 files changed

+114
-13
lines changed

8 files changed

+114
-13
lines changed

.changeset/cruel-baths-sleep.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
---
4+
5+
Fix an issue where `fallbackRedirectUrl` and `forceRedirectUrl` were being improperly passed from sign up to sign in and vice versa. These props will now only apply to the specific flow they were passed to initially.

integration/tests/oauth-flows.test.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('oauth flo
9494
test('sign up modal', async ({ page, context }) => {
9595
const u = createTestUtils({ app, page, context });
9696
// The SignUpModal will only redirect to its provided forceRedirectUrl if the user is signing up; it will not
97-
// redirect if the sign up is transfered to a sign in.
97+
// redirect if the sign up is transferred to a sign in.
9898
await u.services.users.deleteIfExists({ email: fakeUser.email });
9999

100100
await u.page.goToRelative('/buttons');
@@ -114,6 +114,46 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('oauth flo
114114
await u.page.waitForAppUrl('/protected');
115115
});
116116

117+
test('sign in modal to sign up modal to transfer respects original forceRedirectUrl', async ({ page, context }) => {
118+
const u = createTestUtils({ app, page, context });
119+
120+
try {
121+
await u.services.users.createBapiUser(fakeUser);
122+
} catch {
123+
// User already exists, so we don't need to create it
124+
}
125+
126+
await u.page.goToRelative('/buttons');
127+
await u.page.waitForClerkJsLoaded();
128+
await u.po.expect.toBeSignedOut();
129+
130+
// Call clerk.openSignIn with custom redirect URLs
131+
await u.page.evaluate(() => {
132+
window.Clerk.openSignIn({
133+
forceRedirectUrl: '/?from=signin',
134+
signUpForceRedirectUrl: '/?from=signup',
135+
});
136+
});
137+
138+
await u.po.signIn.waitForModal();
139+
140+
// Click the Sign Up button to switch to sign up mode
141+
await u.page.getByRole('dialog').getByRole('link', { name: 'Sign up' }).click();
142+
143+
// Use OAuth provider
144+
await u.page.getByRole('button', { name: 'E2E OAuth Provider' }).click();
145+
await u.page.getByText('Sign in to oauth-provider').waitFor();
146+
147+
// Sign in with existing account
148+
await u.po.signIn.setIdentifier(fakeUser.email);
149+
await u.po.signIn.continue();
150+
await u.po.signIn.enterTestOtpCode();
151+
152+
// Should redirect to the sign in redirect URL since we already had an account
153+
await u.page.waitForAppUrl('/?from=signin');
154+
await u.po.expect.toBeSignedIn();
155+
});
156+
117157
test.describe('authenticateWithPopup', () => {
118158
test('SignIn with oauthFlow=popup opens popup', async ({ page, context }) => {
119159
const u = createTestUtils({ app, page, context });

packages/clerk-js/src/ui/Components.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import React, { Suspense } from 'react';
1818

1919
import { clerkUIErrorDOMElementNotFound } from '../core/errors';
2020
import { buildVirtualRouterUrl } from '../utils';
21+
import { disambiguateRedirectOptions } from '../utils/disambiguateRedirectOptions';
2122
import type { AppearanceCascade } from './customizables/parseAppearance';
2223
// NOTE: Using `./hooks` instead of `./hooks/useClerkModalStateParams` will increase the bundle size
2324
import { useClerkModalStateParams } from './hooks/useClerkModalStateParams';
@@ -396,7 +397,7 @@ const Components = (props: ComponentsProps) => {
396397
componentName={'SignInModal'}
397398
>
398399
<SignInModal {...signInModal} />
399-
<SignUpModal {...signInModal} />
400+
<SignUpModal {...disambiguateRedirectOptions(signInModal, 'signin')} />
400401
<WaitlistModal {...waitlistModal} />
401402
</LazyModalRenderer>
402403
);
@@ -412,7 +413,7 @@ const Components = (props: ComponentsProps) => {
412413
startPath={buildVirtualRouterUrl({ base: '/sign-up', path: urlStateParam?.path })}
413414
componentName={'SignUpModal'}
414415
>
415-
<SignInModal {...signUpModal} />
416+
<SignInModal {...disambiguateRedirectOptions(signUpModal, 'signup')} />
416417
<SignUpModal {...signUpModal} />
417418
<WaitlistModal {...waitlistModal} />
418419
</LazyModalRenderer>

packages/clerk-js/src/ui/components/SignIn/SignIn.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import React from 'react';
55
import { SessionTasks as LazySessionTasks } from '../../../ui/lazyModules/components';
66
import { normalizeRoutingOptions } from '../../../utils/normalizeRoutingOptions';
77
import { SignInEmailLinkFlowComplete, SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard';
8-
import type { SignUpContextType } from '../../contexts';
98
import {
109
SignInContext,
1110
SignUpContext,
@@ -17,6 +16,7 @@ import { Flow } from '../../customizables';
1716
import { useFetch } from '../../hooks';
1817
import { usePreloadTasks } from '../../hooks/usePreloadTasks';
1918
import { Route, Switch, useRouter, VIRTUAL_ROUTER_BASE_PATH } from '../../router';
19+
import type { SignUpCtx } from '../../types';
2020
import {
2121
LazySignUpContinue,
2222
LazySignUpSSOCallback,
@@ -173,7 +173,7 @@ function SignInRoot() {
173173
signInUrl: signInContext.signInUrl,
174174
unsafeMetadata: signInContext.unsafeMetadata,
175175
...normalizeRoutingOptions({ routing: signInContext?.routing, path: signInContext?.path }),
176-
} as SignUpContextType;
176+
} as SignUpCtx;
177177

178178
/**
179179
* Preload Sign Up when in Combined Flow.

packages/clerk-js/src/ui/contexts/components/SignIn.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { useRouter } from '../../router';
1717
import type { SignInCtx } from '../../types';
1818
import { getInitialValuesFromQueryParams } from '../utils';
1919

20-
export type SignInContextType = SignInCtx & {
20+
export type SignInContextType = Omit<SignInCtx, 'fallbackRedirectUrl' | 'forceRedirectUrl'> & {
2121
navigateAfterSignIn: () => any;
2222
queryParams: ParsedQueryString;
2323
signUpUrl: string;
@@ -66,13 +66,16 @@ export const useSignInContext = (): SignInContextType => {
6666
options,
6767
{
6868
...ctx,
69-
signInFallbackRedirectUrl: ctx.fallbackRedirectUrl,
70-
signInForceRedirectUrl: ctx.forceRedirectUrl,
69+
signInFallbackRedirectUrl: ctx.signInFallbackRedirectUrl || ctx.fallbackRedirectUrl,
70+
signInForceRedirectUrl: ctx.signInForceRedirectUrl || ctx.forceRedirectUrl,
7171
},
7272
queryParams,
7373
mode,
7474
);
7575

76+
delete ctx.fallbackRedirectUrl;
77+
delete ctx.forceRedirectUrl;
78+
7679
const afterSignInUrl = clerk.buildUrlWithAuth(redirectUrls.getAfterSignInUrl());
7780
const afterSignUpUrl = clerk.buildUrlWithAuth(redirectUrls.getAfterSignUpUrl());
7881

packages/clerk-js/src/ui/contexts/components/SignUp.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { useRouter } from '../../router';
1717
import type { SignUpCtx } from '../../types';
1818
import { getInitialValuesFromQueryParams } from '../utils';
1919

20-
export type SignUpContextType = SignUpCtx & {
20+
export type SignUpContextType = Omit<SignUpCtx, 'fallbackRedirectUrl' | 'forceRedirectUrl'> & {
2121
navigateAfterSignUp: () => any;
2222
queryParams: ParsedQueryString;
2323
signInUrl: string;
@@ -65,13 +65,16 @@ export const useSignUpContext = (): SignUpContextType => {
6565
options,
6666
{
6767
...ctx,
68-
signUpFallbackRedirectUrl: ctx.fallbackRedirectUrl,
69-
signUpForceRedirectUrl: ctx.forceRedirectUrl,
68+
signUpFallbackRedirectUrl: ctx.signUpFallbackRedirectUrl || ctx.fallbackRedirectUrl,
69+
signUpForceRedirectUrl: ctx.signUpForceRedirectUrl || ctx.forceRedirectUrl,
7070
},
7171
queryParams,
7272
mode,
7373
);
7474

75+
delete ctx.fallbackRedirectUrl;
76+
delete ctx.forceRedirectUrl;
77+
7578
const afterSignUpUrl = clerk.buildUrlWithAuth(redirectUrls.getAfterSignUpUrl());
7679
const afterSignInUrl = clerk.buildUrlWithAuth(redirectUrls.getAfterSignInUrl());
7780

packages/clerk-js/src/ui/types.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import type {
1111
OrganizationListProps,
1212
OrganizationProfileProps,
1313
OrganizationSwitcherProps,
14+
SignInFallbackRedirectUrl,
15+
SignInForceRedirectUrl,
1416
SignInProps,
17+
SignUpFallbackRedirectUrl,
18+
SignUpForceRedirectUrl,
1519
SignUpProps,
1620
UserButtonProps,
1721
UserProfileProps,
@@ -51,7 +55,8 @@ type ComponentMode = 'modal' | 'mounted';
5155
export type SignInCtx = SignInProps & {
5256
componentName: 'SignIn';
5357
mode?: ComponentMode;
54-
};
58+
} & SignInFallbackRedirectUrl &
59+
SignInForceRedirectUrl;
5560

5661
export type UserVerificationCtx = __internal_UserVerificationProps & {
5762
componentName: 'UserVerification';
@@ -68,7 +73,8 @@ export type SignUpCtx = SignUpProps & {
6873
mode?: ComponentMode;
6974
emailLinkRedirectUrl?: string;
7075
ssoCallbackUrl?: string;
71-
};
76+
} & SignUpFallbackRedirectUrl &
77+
SignUpForceRedirectUrl;
7278

7379
export type UserButtonCtx = UserButtonProps & {
7480
componentName: 'UserButton';
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
type Flow = 'signin' | 'signup';
2+
3+
interface RedirectOptions {
4+
forceRedirectUrl?: string | null;
5+
fallbackRedirectUrl?: string | null;
6+
}
7+
8+
type PrefixedRedirectOptions<F extends Flow> = {
9+
[K in keyof RedirectOptions as `${F extends 'signin' ? 'signIn' : 'signUp'}${Capitalize<K>}`]: RedirectOptions[K];
10+
};
11+
12+
type DisambiguatedRedirectOptions<T extends RedirectOptions, F extends Flow> = {
13+
[K in keyof T as K extends 'forceRedirectUrl' | 'fallbackRedirectUrl' ? never : K]: T[K];
14+
} & PrefixedRedirectOptions<F>;
15+
16+
/**
17+
* Ensures redirect props have the appropriate prefix depending on the flow they were originally provided to. Used when passing props from sign up -> sign in, or vice versa, when rendering modals.
18+
*/
19+
export function disambiguateRedirectOptions<T extends RedirectOptions, F extends Flow>(
20+
props: T | null,
21+
flow: F,
22+
): DisambiguatedRedirectOptions<T, F> {
23+
if (!props) {
24+
return {} as DisambiguatedRedirectOptions<T, F>;
25+
}
26+
27+
const prefix = flow === 'signin' ? 'signIn' : 'signUp';
28+
29+
const prefixedOptions = {
30+
[`${prefix}ForceRedirectUrl`]: props.forceRedirectUrl,
31+
[`${prefix}FallbackRedirectUrl`]: props.fallbackRedirectUrl,
32+
} as PrefixedRedirectOptions<F>;
33+
34+
const result = {
35+
...props,
36+
...prefixedOptions,
37+
};
38+
39+
delete result.forceRedirectUrl;
40+
delete result.fallbackRedirectUrl;
41+
42+
return result;
43+
}

0 commit comments

Comments
 (0)