Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/wet-peaches-grow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/clerk-js': patch
'@clerk/clerk-react': patch
---

Respect the `signInForceRedirectUrl`, `signInFallbackRedirectUrl`, `signUpForceRedirectUrl` and `signUpFallbackRedirectUrl` props passed to `SignInButton`, `SignUpButton` and the low-level `window.Clerk.buildSignInUrl` & `window.Clerk.buildSignUpUrl` methods. These props allow you to control the redirect behavior of the `SignIn` and `SignUp` components. For more information, refer to the [Custom Redirects](https://clerk.com/docs/guides/custom-redirects) guide.
32 changes: 16 additions & 16 deletions packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,33 +123,33 @@ describe('Clerk singleton - Redirects', () => {
});

it('redirects to signInUrl for development instance', async () => {
await clerkForDevelopmentInstance.redirectToSignIn({ redirectUrl: 'https://www.example.com/' });
await clerkForDevelopmentInstance.redirectToSignIn({ redirectUrl: '/example' });
expect(mockNavigate).toHaveBeenCalledWith(
'/sign-in#/?redirect_url=https%3A%2F%2Fwww.example.com%2F',
'/sign-in#/?redirect_url=http%3A%2F%2Ftest.host%2Fexample',
undefined,
);
});

it('redirects to signInUrl for production instance', async () => {
await clerkForProductionInstance.redirectToSignIn({ redirectUrl: 'https://www.example.com/' });
await clerkForProductionInstance.redirectToSignIn({ redirectUrl: '/example' });
expect(mockNavigate).toHaveBeenCalledWith(
'/sign-in#/?redirect_url=https%3A%2F%2Fwww.example.com%2F',
'/sign-in#/?redirect_url=http%3A%2F%2Ftest.host%2Fexample',
undefined,
);
});

it('redirects to signUpUrl for development instance', async () => {
await clerkForDevelopmentInstance.redirectToSignUp({ redirectUrl: 'https://www.example.com/' });
await clerkForDevelopmentInstance.redirectToSignUp({ redirectUrl: '/example' });
expect(mockNavigate).toHaveBeenCalledWith(
'/sign-up#/?redirect_url=https%3A%2F%2Fwww.example.com%2F',
'/sign-up#/?redirect_url=http%3A%2F%2Ftest.host%2Fexample',
undefined,
);
});

it('redirects to signUpUrl for production instance', async () => {
await clerkForProductionInstance.redirectToSignUp({ redirectUrl: 'https://www.example.com/' });
await clerkForProductionInstance.redirectToSignUp({ redirectUrl: '/example' });
expect(mockNavigate).toHaveBeenCalledWith(
'/sign-up#/?redirect_url=https%3A%2F%2Fwww.example.com%2F',
'/sign-up#/?redirect_url=http%3A%2F%2Ftest.host%2Fexample',
undefined,
);
});
Expand Down Expand Up @@ -220,31 +220,31 @@ describe('Clerk singleton - Redirects', () => {
const host = 'http://another-test.host';

it('redirects to signInUrl for development instance', async () => {
await clerkForDevelopmentInstance.redirectToSignIn({ redirectUrl: 'https://www.example.com/' });
await clerkForDevelopmentInstance.redirectToSignIn({ redirectUrl: '/example' });
expect(mockHref).toHaveBeenCalledTimes(1);
expect(mockHref).toHaveBeenCalledWith(
`${host}/sign-in?__clerk_db_jwt=deadbeef#/?redirect_url=https%3A%2F%2Fwww.example.com%2F`,
`${host}/sign-in?__clerk_db_jwt=deadbeef#/?redirect_url=http%3A%2F%2Ftest.host%2Fexample`,
);
});

it('redirects to signInUrl for production instance', async () => {
await clerkForProductionInstance.redirectToSignIn({ redirectUrl: 'https://www.example.com/' });
await clerkForProductionInstance.redirectToSignIn({ redirectUrl: '/example' });
expect(mockHref).toHaveBeenCalledTimes(1);
expect(mockHref).toHaveBeenCalledWith(`${host}/sign-in#/?redirect_url=https%3A%2F%2Fwww.example.com%2F`);
expect(mockHref).toHaveBeenCalledWith(`${host}/sign-in#/?redirect_url=http%3A%2F%2Ftest.host%2Fexample`);
});

it('redirects to signUpUrl for development instance', async () => {
await clerkForDevelopmentInstance.redirectToSignUp({ redirectUrl: 'https://www.example.com/' });
await clerkForDevelopmentInstance.redirectToSignUp({ redirectUrl: '/example' });
expect(mockHref).toHaveBeenCalledTimes(1);
expect(mockHref).toHaveBeenCalledWith(
`${host}/sign-up?__clerk_db_jwt=deadbeef#/?redirect_url=https%3A%2F%2Fwww.example.com%2F`,
`${host}/sign-up?__clerk_db_jwt=deadbeef#/?redirect_url=http%3A%2F%2Ftest.host%2Fexample`,
);
});

it('redirects to signUpUrl for production instance', async () => {
await clerkForProductionInstance.redirectToSignUp({ redirectUrl: 'https://www.example.com/' });
await clerkForProductionInstance.redirectToSignUp({ redirectUrl: '/example' });
expect(mockHref).toHaveBeenCalledTimes(1);
expect(mockHref).toHaveBeenCalledWith(`${host}/sign-up#/?redirect_url=https%3A%2F%2Fwww.example.com%2F`);
expect(mockHref).toHaveBeenCalledWith(`${host}/sign-up#/?redirect_url=http%3A%2F%2Ftest.host%2Fexample`);
});

it('redirects to userProfileUrl', async () => {
Expand Down
32 changes: 20 additions & 12 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import type {
OrganizationProfileProps,
OrganizationResource,
OrganizationSwitcherProps,
RedirectOptions,
Resources,
SDKMetadata,
SetActiveParams,
Expand Down Expand Up @@ -826,19 +827,20 @@ export class Clerk implements ClerkInterface {

return setDevBrowserJWTInURL(toURL, devBrowserJwt).href;
}

public buildSignInUrl(options?: SignInRedirectOptions): string {
return this.#buildUrl('signInUrl', {
...options?.initialValues,
redirect_url: options?.redirectUrl || window.location.href,
});
return this.#buildUrl(
'signInUrl',
{ ...options, redirectUrl: options?.redirectUrl || window.location.href },
options?.initialValues,
);
}

public buildSignUpUrl(options?: SignUpRedirectOptions): string {
return this.#buildUrl('signUpUrl', {
...options?.initialValues,
redirect_url: options?.redirectUrl || window.location.href,
});
return this.#buildUrl(
'signUpUrl',
{ ...options, redirectUrl: options?.redirectUrl || window.location.href },
options?.initialValues,
);
}

public buildUserProfileUrl(): string {
Expand Down Expand Up @@ -1642,13 +1644,19 @@ export class Clerk implements ClerkInterface {
});
};

#buildUrl = (key: 'signInUrl' | 'signUpUrl', params?: Record<string, string>): string => {
#buildUrl = (
key: 'signInUrl' | 'signUpUrl',
options: RedirectOptions,
_initValues?: Record<string, string>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙃 The _ prefix in parameters is used to mark unused parameters of a function. Are we sure about this naming?

): string => {
if (!key || !this.loaded || !this.#environment || !this.#environment.displayConfig) {
return '';
}
const signInOrUpUrl = this.#options[key] || this.#environment.displayConfig[key];
const redirectUrls = new RedirectUrls(this.#options, params);
return this.buildUrlWithAuth(redirectUrls.appendPreservedPropsToUrl(signInOrUpUrl, params));
const redirectUrls = new RedirectUrls(this.#options, options).toSearchParams();
const initValues = new URLSearchParams(_initValues || {});
const url = buildURL({ base: signInOrUpUrl, hashSearchParams: [initValues, redirectUrls] }, { stringify: true });
return this.buildUrlWithAuth(url);
};

assertComponentsReady(controls: unknown): asserts controls is ReturnType<MountComponentRenderer> {
Expand Down
11 changes: 7 additions & 4 deletions packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ export const useSignUpContext = (): SignUpContextType => {
let signUpUrl = (ctx.routing === 'path' && ctx.path) || options.signUpUrl || displayConfig.signUpUrl;
let signInUrl = ctx.signInUrl || options.signInUrl || displayConfig.signInUrl;

signUpUrl = redirectUrls.appendPreservedPropsToUrl(signUpUrl, queryParams);
signInUrl = redirectUrls.appendPreservedPropsToUrl(signInUrl, queryParams);
const preservedParams = redirectUrls.getPreservedSearchParams();
signInUrl = buildURL({ base: signInUrl, hashSearchParams: [queryParams, preservedParams] }, { stringify: true });
signUpUrl = buildURL({ base: signUpUrl, hashSearchParams: [queryParams, preservedParams] }, { stringify: true });

// TODO: Avoid building this url again to remove duplicate code. Get it from window.Clerk instead.
const secondFactorUrl = buildURL({ base: signInUrl, hashPath: '/factor-two' }, { stringify: true });
Expand Down Expand Up @@ -161,8 +162,10 @@ export const useSignInContext = (): SignInContextType => {
let signInUrl = (ctx.routing === 'path' && ctx.path) || options.signInUrl || displayConfig.signInUrl;
let signUpUrl = ctx.signUpUrl || options.signUpUrl || displayConfig.signUpUrl;

signInUrl = redirectUrls.appendPreservedPropsToUrl(signInUrl, queryParams);
signUpUrl = redirectUrls.appendPreservedPropsToUrl(signUpUrl, queryParams);
const preservedParams = redirectUrls.getPreservedSearchParams();
signInUrl = buildURL({ base: signInUrl, hashSearchParams: [queryParams, preservedParams] }, { stringify: true });
signUpUrl = buildURL({ base: signUpUrl, hashSearchParams: [queryParams, preservedParams] }, { stringify: true });

const signUpContinueUrl = buildURL({ base: signUpUrl, hashPath: '/continue' }, { stringify: true });

return {
Expand Down
3 changes: 1 addition & 2 deletions packages/clerk-js/src/ui/router/RouteContext.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { ParsedQs } from 'qs';
Comment thread
nikosdouvlis marked this conversation as resolved.
import React from 'react';

export interface RouteContextValue {
Expand All @@ -15,7 +14,7 @@ export interface RouteContextValue {
refresh: () => void;
params: { [key: string]: string };
queryString: string;
queryParams: ParsedQs;
queryParams: Record<string, string>;
preservedParams?: string[];
getMatchData: (path?: string, index?: boolean) => false | object;
urlStateParam?: {
Expand Down
61 changes: 14 additions & 47 deletions packages/clerk-js/src/utils/__tests__/redirectUrls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ describe('redirectUrls', () => {
});

describe('search params', () => {
it('appends only the preserved props', () => {
it('flattens and returns all params', () => {
const redirectUrls = new RedirectUrls(
{
signInFallbackRedirectUrl: 'sign-in-fallback-redirect-url',
Expand All @@ -263,66 +263,33 @@ describe('redirectUrls', () => {
);

const params = redirectUrls.toSearchParams();
expect([...params.keys()].length).toBe(1);
expect([...params.keys()].length).toBe(3);
expect(params.get('sign_in_force_redirect_url')).toBe(
`${mockWindowLocation.href}props-sign-in-force-redirect-url`,
);
expect(params.get('sign_up_fallback_redirect_url')).toBe(
`${mockWindowLocation.href}search-param-sign-up-fallback-redirect-url`,
);
expect(params.get('redirect_url')).toBe(`${mockWindowLocation.href}search-param-redirect-url`);
});
});

describe('append to url', () => {
it('does not append redirect urls from options to the url if the url is same origin', () => {
describe('preserved search params', () => {
it('does not return redirect urls if they are not in the preserved props array', () => {
const redirectUrls = new RedirectUrls({
signInFallbackRedirectUrl: 'sign-in-fallback-redirect-url',
signUpFallbackRedirectUrl: 'sign-up-fallback-redirect-url',
});

const url = redirectUrls.appendPreservedPropsToUrl('https://www.clerk.com');
expect(url).toBe('https://www.clerk.com/');
});

it('appends redirect urls from options to the url if the url is cross origin', () => {
const redirectUrls = new RedirectUrls({}, {}, { redirect_url: '/search-param-redirect-url' });

const url = redirectUrls.appendPreservedPropsToUrl('https://www.example.com');
expect(url).toContain('search-param-redirect-url');
});

it('overrides the existing search params', () => {
const redirectUrls = new RedirectUrls(
{
signInFallbackRedirectUrl: 'sign-in-fallback-redirect-url',
signUpFallbackRedirectUrl: 'sign-up-fallback-redirect-url',
},
{},
{ redirect_url: '/search-param-redirect-url' },
);

const url = redirectUrls.appendPreservedPropsToUrl('https://www.example.com?redirect_url=existing');
expect(url).toBe(
'https://www.example.com/?redirect_url=existing#/?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fsearch-param-redirect-url',
);
const params = redirectUrls.getPreservedSearchParams();
expect([...params.keys()].length).toBe(0);
});

it('appends redirect urls from props to the url even if the url is same origin', () => {
const redirectUrls = new RedirectUrls({}, {}, { redirect_url: '/search-param-redirect-url' });

const url = redirectUrls.appendPreservedPropsToUrl('https://www.clerk.com');
expect(url).toContain('search-param-redirect-url');
});

it('does not append redirect urls from props to the url if the url is same origin if they match the options urls', () => {
const redirectUrls = new RedirectUrls(
{
signInFallbackRedirectUrl: 'sign-in-fallback-redirect-url',
signUpFallbackRedirectUrl: 'sign-up-fallback-redirect-url',
},
{
signInFallbackRedirectUrl: 'sign-in-fallback-redirect-url',
signUpFallbackRedirectUrl: 'sign-up-fallback-redirect-url',
},
);

const url = redirectUrls.appendPreservedPropsToUrl('https://www.clerk.com');
expect(url).toBe('https://www.clerk.com/');
const params = redirectUrls.getPreservedSearchParams();
expect(params.get('redirect_url')).toContain('search-param-redirect-url');
});
});
});
Loading