Skip to content

Commit bab2e7e

Browse files
authored
feat(clerk-js,types,shared): Introduce force and fallback redirect urls (#3162)
1 parent 8913296 commit bab2e7e

34 files changed

+944
-588
lines changed

.changeset/friendly-masks-obey.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
'@clerk/shared': patch
4+
'@clerk/types': patch
5+
---
6+
7+
Introduce forceRedirectUrl and fallbackRedirectUrl

.github/workflows/release.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
playwright-enabled: true # Must be present to enable caching on branched workflows
5050

5151
- name: Build release
52-
run: npx turbo build $TURBO_ARGS --force
52+
run: npx turbo build $TURBO_ARGS --force --filter=!elements
5353

5454
- name: Create Release PR
5555
id: changesets

integration/playwright.config.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ export default defineConfig({
4343
use: { ...devices['Desktop Chrome'], channel: 'chrome' },
4444
dependencies: ['setup'],
4545
},
46-
{
47-
name: 'webkit',
48-
use: { ...devices['Desktop Safari'] },
49-
dependencies: ['setup'],
50-
},
46+
// {
47+
// name: 'webkit',
48+
// use: { ...devices['Desktop Safari'] },
49+
// dependencies: ['setup'],
50+
// },
5151
],
5252
});

integration/tests/navigation.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export default function Page() {
112112

113113
await u.po.signIn.getGoToSignUp().click();
114114
await u.po.signUp.waitForMounted();
115-
await u.page.waitForURL(`${app.serverUrl}/sign-up?redirect_url=${encodeURIComponent(app.serverUrl + '/')}`);
115+
await u.page.waitForURL(`${app.serverUrl}/sign-up`);
116116

117117
await page.goBack();
118118
await u.po.signIn.waitForMounted();

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
]
1010
},
1111
"scripts": {
12-
"build": "FORCE_COLOR=1 turbo build --concurrency=${TURBO_CONCURRENCY:-80%}",
12+
"build": "FORCE_COLOR=1 turbo build --concurrency=${TURBO_CONCURRENCY:-80%} --filter=!@clerk/elements",
1313
"bundlewatch": "turbo bundlewatch",
1414
"changeset": "changeset",
1515
"changeset:empty": "npm run changeset -- --empty",
@@ -28,7 +28,7 @@
2828
"release:canary": "changeset publish --tag canary --no-git-tag",
2929
"release:snapshot": "changeset publish --tag snapshot --no-git-tag",
3030
"release:verdaccio": "if [ \"$(npm config get registry)\" = \"https://registry.npmjs.org/\" ]; then echo 'Error: Using default registry' && exit 1; else TURBO_CONCURRENCY=1 npm run build && changeset publish --no-git-tag; fi",
31-
"test": "FORCE_COLOR=1 turbo test --concurrency=${TURBO_CONCURRENCY:-80%}",
31+
"test": "FORCE_COLOR=1 turbo test --concurrency=${TURBO_CONCURRENCY:-80%} --filter=!@clerk/elements",
3232
"test:cache:clear": "FORCE_COLOR=1 turbo test:cache:clear --continue --concurrency=${TURBO_CONCURRENCY:-80%}",
3333
"test:integration:ap-flows": "npm run test:integration:base -- --grep @ap-flows",
3434
"test:integration:base": "DEBUG=1 npx playwright test --config integration/playwright.config.ts",

packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts

+42-21
Original file line numberDiff line numberDiff line change
@@ -122,20 +122,36 @@ describe('Clerk singleton - Redirects', () => {
122122
mockEnvironmentFetch.mockRestore();
123123
});
124124

125-
it('redirects to signInUrl', async () => {
126-
await clerkForProductionInstance.redirectToSignIn({ redirectUrl: 'https://www.example.com/' });
125+
it('redirects to signInUrl for development instance', async () => {
127126
await clerkForDevelopmentInstance.redirectToSignIn({ redirectUrl: 'https://www.example.com/' });
127+
expect(mockNavigate).toHaveBeenCalledWith(
128+
'/sign-in#/?redirect_url=https%3A%2F%2Fwww.example.com%2F',
129+
undefined,
130+
);
131+
});
128132

129-
expect(mockNavigate.mock.calls[0][0]).toBe('/sign-in#/?redirect_url=https%3A%2F%2Fwww.example.com%2F');
130-
expect(mockNavigate.mock.calls[1][0]).toBe('/sign-in#/?redirect_url=https%3A%2F%2Fwww.example.com%2F');
133+
it('redirects to signInUrl for production instance', async () => {
134+
await clerkForProductionInstance.redirectToSignIn({ redirectUrl: 'https://www.example.com/' });
135+
expect(mockNavigate).toHaveBeenCalledWith(
136+
'/sign-in#/?redirect_url=https%3A%2F%2Fwww.example.com%2F',
137+
undefined,
138+
);
131139
});
132140

133-
it('redirects to signUpUrl', async () => {
134-
await clerkForProductionInstance.redirectToSignUp({ redirectUrl: 'https://www.example.com/' });
141+
it('redirects to signUpUrl for development instance', async () => {
135142
await clerkForDevelopmentInstance.redirectToSignUp({ redirectUrl: 'https://www.example.com/' });
143+
expect(mockNavigate).toHaveBeenCalledWith(
144+
'/sign-up#/?redirect_url=https%3A%2F%2Fwww.example.com%2F',
145+
undefined,
146+
);
147+
});
136148

137-
expect(mockNavigate.mock.calls[0][0]).toBe('/sign-up#/?redirect_url=https%3A%2F%2Fwww.example.com%2F');
138-
expect(mockNavigate.mock.calls[1][0]).toBe('/sign-up#/?redirect_url=https%3A%2F%2Fwww.example.com%2F');
149+
it('redirects to signUpUrl for production instance', async () => {
150+
await clerkForProductionInstance.redirectToSignUp({ redirectUrl: 'https://www.example.com/' });
151+
expect(mockNavigate).toHaveBeenCalledWith(
152+
'/sign-up#/?redirect_url=https%3A%2F%2Fwww.example.com%2F',
153+
undefined,
154+
);
139155
});
140156

141157
it('redirects to userProfileUrl', async () => {
@@ -203,29 +219,34 @@ describe('Clerk singleton - Redirects', () => {
203219

204220
const host = 'http://another-test.host';
205221

206-
it('redirects to signInUrl', async () => {
207-
await clerkForProductionInstance.redirectToSignIn({ redirectUrl: 'https://www.example.com/' });
222+
it('redirects to signInUrl for development instance', async () => {
208223
await clerkForDevelopmentInstance.redirectToSignIn({ redirectUrl: 'https://www.example.com/' });
209-
210-
expect(mockHref).toHaveBeenNthCalledWith(1, `${host}/sign-in#/?redirect_url=https%3A%2F%2Fwww.example.com%2F`);
211-
212-
expect(mockHref).toHaveBeenNthCalledWith(
213-
2,
224+
expect(mockHref).toHaveBeenCalledTimes(1);
225+
expect(mockHref).toHaveBeenCalledWith(
214226
`${host}/sign-in?__clerk_db_jwt=deadbeef#/?redirect_url=https%3A%2F%2Fwww.example.com%2F`,
215227
);
216228
});
217229

218-
it('redirects to signUpUrl', async () => {
219-
await clerkForProductionInstance.redirectToSignUp({ redirectUrl: 'https://www.example.com/' });
220-
await clerkForDevelopmentInstance.redirectToSignUp({ redirectUrl: 'https://www.example.com/' });
230+
it('redirects to signInUrl for production instance', async () => {
231+
await clerkForProductionInstance.redirectToSignIn({ redirectUrl: 'https://www.example.com/' });
232+
expect(mockHref).toHaveBeenCalledTimes(1);
233+
expect(mockHref).toHaveBeenCalledWith(`${host}/sign-in#/?redirect_url=https%3A%2F%2Fwww.example.com%2F`);
234+
});
221235

222-
expect(mockHref).toHaveBeenNthCalledWith(1, `${host}/sign-up#/?redirect_url=https%3A%2F%2Fwww.example.com%2F`);
223-
expect(mockHref).toHaveBeenNthCalledWith(
224-
2,
236+
it('redirects to signUpUrl for development instance', async () => {
237+
await clerkForDevelopmentInstance.redirectToSignUp({ redirectUrl: 'https://www.example.com/' });
238+
expect(mockHref).toHaveBeenCalledTimes(1);
239+
expect(mockHref).toHaveBeenCalledWith(
225240
`${host}/sign-up?__clerk_db_jwt=deadbeef#/?redirect_url=https%3A%2F%2Fwww.example.com%2F`,
226241
);
227242
});
228243

244+
it('redirects to signUpUrl for production instance', async () => {
245+
await clerkForProductionInstance.redirectToSignUp({ redirectUrl: 'https://www.example.com/' });
246+
expect(mockHref).toHaveBeenCalledTimes(1);
247+
expect(mockHref).toHaveBeenCalledWith(`${host}/sign-up#/?redirect_url=https%3A%2F%2Fwww.example.com%2F`);
248+
});
249+
229250
it('redirects to userProfileUrl', async () => {
230251
await clerkForProductionInstance.redirectToUserProfile();
231252
await clerkForDevelopmentInstance.redirectToUserProfile();

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -981,7 +981,7 @@ describe('Clerk singleton', () => {
981981
});
982982
});
983983

984-
it('redirects the user to the afterSignInUrl if one was provided', async () => {
984+
it('redirects the user to the signInForceRedirectUrl if one was provided', async () => {
985985
mockEnvironmentFetch.mockReturnValue(
986986
Promise.resolve({
987987
authConfig: {},
@@ -1028,15 +1028,15 @@ describe('Clerk singleton', () => {
10281028
sut.setActive = mockSetActive as any;
10291029

10301030
sut.handleRedirectCallback({
1031-
redirectUrl: '/custom-sign-in',
1031+
signInForceRedirectUrl: '/custom-sign-in',
10321032
});
10331033

10341034
await waitFor(() => {
10351035
expect(mockNavigate.mock.calls[0][0]).toBe('/custom-sign-in');
10361036
});
10371037
});
10381038

1039-
it('gives priority to afterSignInUrl if afterSignInUrl and redirectUrl were provided ', async () => {
1039+
it('gives priority to signInForceRedirectUrl if signInForceRedirectUrl and signInFallbackRedirectUrl were provided ', async () => {
10401040
mockEnvironmentFetch.mockReturnValue(
10411041
Promise.resolve({
10421042
authConfig: {},
@@ -1083,8 +1083,8 @@ describe('Clerk singleton', () => {
10831083
sut.setActive = mockSetActive as any;
10841084

10851085
sut.handleRedirectCallback({
1086-
afterSignInUrl: '/custom-sign-in',
1087-
redirectUrl: '/redirect-to',
1086+
signInForceRedirectUrl: '/custom-sign-in',
1087+
signInFallbackRedirectUrl: '/redirect-to',
10881088
} as any);
10891089

10901090
await waitFor(() => {

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

+26-46
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ import type {
3737
OrganizationProfileProps,
3838
OrganizationResource,
3939
OrganizationSwitcherProps,
40-
RedirectOptions,
4140
Resources,
4241
SDKMetadata,
4342
SetActiveParams,
@@ -59,7 +58,6 @@ import type {
5958

6059
import type { MountComponentRenderer } from '../ui/Components';
6160
import {
62-
appendAsQueryParams,
6361
buildURL,
6462
completeSignUpFlow,
6563
createAllowedRedirectOrigins,
@@ -77,17 +75,16 @@ import {
7775
isRedirectForFAPIInitiatedFlow,
7876
noOrganizationExists,
7977
noUserExists,
80-
pickRedirectionProp,
8178
removeClerkQueryParam,
8279
requiresUserInput,
8380
sessionExistsAndSingleSessionModeEnabled,
8481
stripOrigin,
85-
stripSameOrigin,
86-
toURL,
8782
windowNavigate,
8883
} from '../utils';
84+
import { assertNoLegacyProp } from '../utils/assertNoLegacyProp';
8985
import { getClientUatCookie } from '../utils/cookies/clientUat';
9086
import { memoizeListenerCallback } from '../utils/memoizeStateListenerCallback';
87+
import { RedirectUrls } from '../utils/redirectUrls';
9188
import { CLERK_SATELLITE_URL, CLERK_SYNCED, ERROR_CODES } from './constants';
9289
import type { DevBrowser } from './devBrowser';
9390
import { createDevBrowser } from './devBrowser';
@@ -133,9 +130,11 @@ const defaultOptions: ClerkOptions = {
133130
isSatellite: false,
134131
signInUrl: undefined,
135132
signUpUrl: undefined,
136-
afterSignInUrl: undefined,
137-
afterSignUpUrl: undefined,
138133
afterSignOutUrl: undefined,
134+
signInFallbackRedirectUrl: undefined,
135+
signUpFallbackRedirectUrl: undefined,
136+
signInForceRedirectUrl: undefined,
137+
signUpForceRedirectUrl: undefined,
139138
};
140139

141140
export class Clerk implements ClerkInterface {
@@ -276,6 +275,8 @@ export class Clerk implements ClerkInterface {
276275
...options,
277276
};
278277

278+
assertNoLegacyProp(this.#options);
279+
279280
if (this.#options.sdkMetadata) {
280281
Clerk.sdkMetadata = this.#options.sdkMetadata;
281282
}
@@ -827,11 +828,17 @@ export class Clerk implements ClerkInterface {
827828
}
828829

829830
public buildSignInUrl(options?: SignInRedirectOptions): string {
830-
return this.#buildUrl('signInUrl', options);
831+
return this.#buildUrl('signInUrl', {
832+
...options?.initialValues,
833+
redirect_url: options?.redirectUrl || window.location.href,
834+
});
831835
}
832836

833837
public buildSignUpUrl(options?: SignUpRedirectOptions): string {
834-
return this.#buildUrl('signUpUrl', options);
838+
return this.#buildUrl('signUpUrl', {
839+
...options?.initialValues,
840+
redirect_url: options?.redirectUrl || window.location.href,
841+
});
835842
}
836843

837844
public buildUserProfileUrl(): string {
@@ -849,19 +856,11 @@ export class Clerk implements ClerkInterface {
849856
}
850857

851858
public buildAfterSignInUrl(): string {
852-
if (!this.#options.afterSignInUrl) {
853-
return '/';
854-
}
855-
856-
return this.buildUrlWithAuth(this.#options.afterSignInUrl);
859+
return this.buildUrlWithAuth(new RedirectUrls(this.#options).getAfterSignInUrl());
857860
}
858861

859862
public buildAfterSignUpUrl(): string {
860-
if (!this.#options.afterSignUpUrl) {
861-
return '/';
862-
}
863-
864-
return this.buildUrlWithAuth(this.#options.afterSignUpUrl);
863+
return this.buildUrlWithAuth(new RedirectUrls(this.#options).getAfterSignUpUrl());
865864
}
866865

867866
public buildAfterSignOutUrl(): string {
@@ -1062,9 +1061,9 @@ export class Clerk implements ClerkInterface {
10621061
buildURL({ base: displayConfig.signInUrl, hashPath: '/reset-password' }, { stringify: true }),
10631062
);
10641063

1065-
const navigateAfterSignIn = makeNavigate(params.afterSignInUrl || params.redirectUrl || '/');
1066-
1067-
const navigateAfterSignUp = makeNavigate(params.afterSignUpUrl || params.redirectUrl || '/');
1064+
const redirectUrls = new RedirectUrls(this.#options, params);
1065+
const navigateAfterSignIn = makeNavigate(redirectUrls.getAfterSignInUrl());
1066+
const navigateAfterSignUp = makeNavigate(redirectUrls.getAfterSignUpUrl());
10681067

10691068
const navigateToContinueSignUp = makeNavigate(
10701069
params.continueSignUpUrl ||
@@ -1090,7 +1089,6 @@ export class Clerk implements ClerkInterface {
10901089

10911090
const userExistsButNeedsToSignIn =
10921091
su.externalAccountStatus === 'transferable' && su.externalAccountErrorCode === 'external_account_exists';
1093-
10941092
if (userExistsButNeedsToSignIn) {
10951093
const res = await signIn.create({ transfer: true });
10961094
switch (res.status) {
@@ -1644,31 +1642,13 @@ export class Clerk implements ClerkInterface {
16441642
});
16451643
};
16461644

1647-
#buildUrl = (key: 'signInUrl' | 'signUpUrl', options?: SignInRedirectOptions | SignUpRedirectOptions): string => {
1648-
if (!this.loaded || !this.#environment || !this.#environment.displayConfig) {
1645+
#buildUrl = (key: 'signInUrl' | 'signUpUrl', params?: Record<string, string>): string => {
1646+
if (!key || !this.loaded || !this.#environment || !this.#environment.displayConfig) {
16491647
return '';
16501648
}
1651-
1652-
const signInOrUpUrl = pickRedirectionProp(
1653-
key,
1654-
{ options: this.#options, displayConfig: this.#environment.displayConfig },
1655-
false,
1656-
);
1657-
1658-
const urls: RedirectOptions = {
1659-
afterSignInUrl: pickRedirectionProp('afterSignInUrl', { ctx: options, options: this.#options }, false),
1660-
afterSignUpUrl: pickRedirectionProp('afterSignUpUrl', { ctx: options, options: this.#options }, false),
1661-
redirectUrl: options?.redirectUrl || window.location.href,
1662-
};
1663-
1664-
(Object.keys(urls) as Array<keyof typeof urls>).forEach(function (key) {
1665-
const url = urls[key];
1666-
if (url) {
1667-
urls[key] = stripSameOrigin(toURL(url), toURL(signInOrUpUrl));
1668-
}
1669-
});
1670-
1671-
return this.buildUrlWithAuth(appendAsQueryParams(signInOrUpUrl, { ...urls, ...options?.initialValues }));
1649+
const signInOrUpUrl = this.#options[key] || this.#environment.displayConfig[key];
1650+
const redirectUrls = new RedirectUrls(this.#options, params);
1651+
return this.buildUrlWithAuth(redirectUrls.appendPreservedPropsToUrl(signInOrUpUrl, params));
16721652
};
16731653

16741654
assertComponentsReady(controls: unknown): asserts controls is ReturnType<MountComponentRenderer> {

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

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
export const PRESERVED_QUERYSTRING_PARAMS = ['after_sign_in_url', 'after_sign_up_url', 'redirect_url'];
1+
// TODO: Do we still have a use for this or can we simply preserve all params?
2+
export const PRESERVED_QUERYSTRING_PARAMS = [
3+
'redirect_url',
4+
'sign_in_force_redirect_url',
5+
'sign_in_fallback_redirect_url',
6+
'sign_up_force_redirect_url',
7+
'sign_up_fallback_redirect_url',
8+
];
29

310
export const CLERK_MODAL_STATE = '__clerk_modal_state';
411
export const CLERK_SYNCED = '__clerk_synced';

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

+3-4
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,8 @@ function SignInRoutes(): JSX.Element {
4444
<SignInSSOCallback
4545
signUpUrl={signInContext.signUpUrl}
4646
signInUrl={signInContext.signInUrl}
47-
afterSignInUrl={signInContext.afterSignInUrl}
48-
afterSignUpUrl={signInContext.afterSignUpUrl}
49-
redirectUrl={signInContext.redirectUrl}
47+
signInForceRedirectUrl={signInContext.afterSignInUrl}
48+
signUpForceRedirectUrl={signInContext.afterSignUpUrl}
5049
continueSignUpUrl={signInContext.signUpContinueUrl}
5150
firstFactorUrl={'../factor-one'}
5251
secondFactorUrl={'../factor-two'}
@@ -58,7 +57,7 @@ function SignInRoutes(): JSX.Element {
5857
</Route>
5958
<Route path='verify'>
6059
<SignInEmailLinkFlowComplete
61-
redirectUrlComplete={signInContext.afterSignInUrl || signInContext.redirectUrl || undefined}
60+
redirectUrlComplete={signInContext.afterSignInUrl}
6261
redirectUrl='../factor-two'
6362
/>
6463
</Route>

packages/clerk-js/src/ui/components/SignIn/__tests__/SignInStart.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ describe('SignInStart', () => {
199199
expect(fixtures.signIn.create).toHaveBeenCalled();
200200
expect(fixtures.signIn.authenticateWithRedirect).toHaveBeenCalledWith({
201201
strategy: 'saml',
202-
redirectUrl: 'http://localhost/#/sso-callback?redirect_url=http%3A%2F%2Flocalhost%2F',
202+
redirectUrl: 'http://localhost/#/sso-callback',
203203
redirectUrlComplete: '/',
204204
});
205205
});

0 commit comments

Comments
 (0)