Skip to content

Commit

Permalink
fix(clerk-js): Respect props passed to buildSignInUrl and `buildSig…
Browse files Browse the repository at this point in the history
…nUpUrl` (#3361)

* fix(clerk-react): Correctly map redirect prop names

* fix(clerk-js): Respect props passed to buildSignInUrl and buildSignUpUrl across navigations
  • Loading branch information
nikosdouvlis committed May 13, 2024
1 parent 6f61130 commit bcbb2c9
Show file tree
Hide file tree
Showing 14 changed files with 215 additions and 213 deletions.
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>,
): 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';
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');
});
});
});

0 comments on commit bcbb2c9

Please sign in to comment.