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
18 changes: 18 additions & 0 deletions .changeset/selfish-flies-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@clerk/clerk-js': minor
'@clerk/clerk-react': minor
'@clerk/types': minor
---

Update `@clerk/clerk-js` and `@clerk/clerk-react` to support the following examples:

```typescript
Clerk.signOut({ redirectUrl: '/' })

<SignOutButton redirectUrl='/' />
// uses Clerk.signOut({ redirectUrl: '/' })
<UserButton afterSignOutUrl='/after' />
// uses Clerk.signOut({ redirectUrl: '/after' })
<ClerkProvider afterSignOutUrl='/after' />
// uses Clerk.signOut({ redirectUrl: '/after' })
```
32 changes: 29 additions & 3 deletions packages/clerk-js/src/core/clerk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,10 @@ describe('Clerk singleton', () => {
await sut.signOut();
await waitFor(() => {
expect(mockClientDestroy).toHaveBeenCalled();
expect(sut.setActive).toHaveBeenCalledWith({ session: null });
expect(sut.setActive).toHaveBeenCalledWith({
session: null,
beforeEmit: expect.any(Function),
});
});
});

Expand All @@ -463,7 +466,7 @@ describe('Clerk singleton', () => {
await waitFor(() => {
expect(mockClientDestroy).toHaveBeenCalled();
expect(mockSession1.remove).not.toHaveBeenCalled();
expect(sut.setActive).toHaveBeenCalledWith({ session: null });
expect(sut.setActive).toHaveBeenCalledWith({ session: null, beforeEmit: expect.any(Function) });
});
});

Expand All @@ -485,6 +488,7 @@ describe('Clerk singleton', () => {
expect(mockClientDestroy).not.toHaveBeenCalled();
expect(sut.setActive).not.toHaveBeenCalledWith({
session: null,
beforeEmit: expect.any(Function),
});
});
});
Expand All @@ -505,7 +509,29 @@ describe('Clerk singleton', () => {
await waitFor(() => {
expect(mockSession1.remove).toHaveBeenCalled();
expect(mockClientDestroy).not.toHaveBeenCalled();
expect(sut.setActive).toHaveBeenCalledWith({ session: null });
expect(sut.setActive).toHaveBeenCalledWith({ session: null, beforeEmit: expect.any(Function) });
});
});

it('removes and signs out the session and redirects to the provided redirectUrl ', async () => {
mockClientFetch.mockReturnValue(
Promise.resolve({
activeSessions: [mockSession1, mockSession2],
sessions: [mockSession1, mockSession2],
destroy: mockClientDestroy,
}),
);

const sut = new Clerk(productionPublishableKey);
sut.setActive = jest.fn(({ beforeEmit }) => beforeEmit());
sut.navigate = jest.fn();
await sut.load();
await sut.signOut({ sessionId: '1', redirectUrl: '/after-sign-out' });
await waitFor(() => {
expect(mockSession1.remove).toHaveBeenCalled();
expect(mockClientDestroy).not.toHaveBeenCalled();
expect(sut.setActive).toHaveBeenCalledWith({ session: null, beforeEmit: expect.any(Function) });
expect(sut.navigate).toHaveBeenCalledWith('/after-sign-out');
});
});
});
Expand Down
21 changes: 20 additions & 1 deletion packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ const defaultOptions: ClerkOptions = {
signUpUrl: undefined,
afterSignInUrl: undefined,
afterSignUpUrl: undefined,
afterSignOutUrl: undefined,
};

export class Clerk implements ClerkInterface {
Expand Down Expand Up @@ -301,9 +302,12 @@ export class Clerk implements ClerkInterface {
if (!this.client || this.client.sessions.length === 0) {
return;
}
const cb = typeof callbackOrOptions === 'function' ? callbackOrOptions : undefined;
const opts = callbackOrOptions && typeof callbackOrOptions === 'object' ? callbackOrOptions : options || {};

const redirectUrl = opts?.redirectUrl || this.buildAfterSignOutUrl();
const defaultCb = () => this.navigate(redirectUrl);
const cb = typeof callbackOrOptions === 'function' ? callbackOrOptions : defaultCb;

if (!opts.sessionId || this.client.activeSessions.length === 1) {
await this.client.destroy();
return this.setActive({
Expand Down Expand Up @@ -758,6 +762,14 @@ export class Clerk implements ClerkInterface {
return this.buildUrlWithAuth(this.#options.afterSignUpUrl);
}

public buildAfterSignOutUrl(): string {
if (!this.#options.afterSignOutUrl) {
return '/';
}

return this.buildUrlWithAuth(this.#options.afterSignOutUrl);
}

public buildCreateOrganizationUrl(): string {
if (!this.#environment || !this.#environment.displayConfig) {
return '';
Expand Down Expand Up @@ -848,6 +860,13 @@ export class Clerk implements ClerkInterface {
return;
};

public redirectToAfterSignOut = async (): Promise<unknown> => {
if (inBrowser()) {
return this.navigate(this.buildAfterSignOutUrl());
}
return;
};

public handleEmailLinkVerification = async (
params: HandleEmailLinkVerificationParams,
customNavigate?: (to: string) => Promise<unknown>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,34 @@ describe('UserButton', () => {
email_addresses: ['test@clerk.com'],
});
});

fixtures.clerk.signOut.mockImplementationOnce(callback => callback());

const { getByText, getByRole, userEvent } = render(<UserButton />, { wrapper });
await userEvent.click(getByRole('button', { name: 'Open user button' }));
await userEvent.click(getByText('Sign out'));

expect(fixtures.router.navigate).toHaveBeenCalledWith('/');
});

it('redirects to afterSignOutUrl when "Sign out" is clicked and afterSignOutUrl prop is passed', async () => {
const { wrapper, fixtures, props } = await createFixtures(f => {
f.withUser({
first_name: 'First',
last_name: 'Last',
username: 'username1',
email_addresses: ['test@clerk.com'],
});
});

fixtures.clerk.signOut.mockImplementation(callback => callback());
props.setProps({ afterSignOutUrl: '/after-sign-out' });

const { getByText, getByRole, userEvent } = render(<UserButton />, { wrapper });
await userEvent.click(getByRole('button', { name: 'Open user button' }));
await userEvent.click(getByText('Sign out'));
expect(fixtures.clerk.signOut).toHaveBeenCalled();

expect(fixtures.router.navigate).toHaveBeenCalledWith('/after-sign-out');
});

it.todo('navigates to sign in url when "Add account" is clicked');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ export const useUserButtonContext = () => {
const afterMultiSessionSingleSignOutUrl = ctx.afterMultiSessionSingleSignOutUrl || displayConfig.afterSignOutOneUrl;
const navigateAfterMultiSessionSingleSignOut = () => clerk.redirectWithAuth(afterMultiSessionSingleSignOutUrl);

const afterSignOutUrl = ctx.afterSignOutUrl || '/';
const afterSignOutUrl = ctx.afterSignOutUrl || clerk.buildAfterSignOutUrl();
const navigateAfterSignOut = () => navigate(afterSignOutUrl);

const afterSwitchSessionUrl = ctx.afterSwitchSessionUrl || displayConfig.afterSwitchSessionUrl;
Expand Down
9 changes: 1 addition & 8 deletions packages/react/src/components/SignOutButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,7 @@ export const SignOutButton = withClerk(
children = normalizeWithDefaultValue(children, 'Sign out');
const child = assertSingleChild(children)('SignOutButton');

const navigateToRedirectUrl = () => {
return clerk.navigate(redirectUrl);
};

const clickHandler = () => {
return clerk.signOut(navigateToRedirectUrl, signOutOptions);
};

const clickHandler = () => clerk.signOut({ redirectUrl });
const wrappedChildClickHandler: React.MouseEventHandler = async e => {
await safeExecute((child as any).props.onClick)(e);
return clickHandler();
Expand Down
21 changes: 21 additions & 0 deletions packages/react/src/isomorphicClerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ type IsomorphicLoadedClerk = Without<
| 'buildOrganizationProfileUrl'
| 'buildAfterSignUpUrl'
| 'buildAfterSignInUrl'
| 'buildAfterSignOutUrl'
| 'buildUrlWithAuth'
| 'handleRedirectCallback'
| 'handleUnauthenticated'
Expand Down Expand Up @@ -115,6 +116,8 @@ type IsomorphicLoadedClerk = Without<
buildAfterSignInUrl: () => string | void;
// TODO: Align return type
buildAfterSignUpUrl: () => string | void;
// TODO: Align return type
buildAfterSignOutUrl: () => string | void;
// TODO: Align optional props
mountUserButton: (node: HTMLDivElement, props: UserButtonProps) => void;
mountOrganizationList: (node: HTMLDivElement, props: OrganizationListProps) => void;
Expand Down Expand Up @@ -278,6 +281,15 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
}
};

buildAfterSignOutUrl = (): string | void => {
const callback = () => this.clerkjs?.buildAfterSignOutUrl() || '';
if (this.clerkjs && this.#loaded) {
return callback();
} else {
this.premountMethodCalls.set('buildAfterSignOutUrl', callback);
}
};

buildUserProfileUrl = (): string | void => {
const callback = () => this.clerkjs?.buildUserProfileUrl() || '';
if (this.clerkjs && this.#loaded) {
Expand Down Expand Up @@ -835,6 +847,15 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
}
};

redirectToAfterSignOut = (): void => {
const callback = () => this.clerkjs?.redirectToAfterSignOut();
if (this.clerkjs && this.#loaded) {
callback();
} else {
this.premountMethodCalls.set('redirectToAfterSignOut', callback);
}
};

redirectToOrganizationProfile = async (): Promise<unknown> => {
const callback = () => this.clerkjs?.redirectToOrganizationProfile();
if (this.clerkjs && this.#loaded) {
Expand Down
94 changes: 52 additions & 42 deletions packages/types/src/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ export type SignOutOptions = {
* multi-session applications.
*/
sessionId?: string;
/**
* Specify a redirect URL to navigate after sign out is complete.
*/
redirectUrl?: string;
};

export interface SignOut {
Expand Down Expand Up @@ -341,15 +345,20 @@ export interface Clerk {
buildOrganizationProfileUrl(): string;

/**
* Returns the configured afterSignIn url of the instance.
* Returns the configured afterSignInUrl of the instance.
*/
buildAfterSignInUrl(): string;

/**
* Returns the configured afterSignIn url of the instance.
* Returns the configured afterSignInUrl of the instance.
*/
buildAfterSignUpUrl(): string;

/**
* Returns the configured afterSignOutUrl of the instance.
*/
buildAfterSignOutUrl(): string;

/**
*
* Redirects to the provided url after decorating it with the auth token for development instances.
Expand Down Expand Up @@ -397,6 +406,11 @@ export interface Clerk {
*/
redirectToAfterSignUp: () => void;

/**
* Redirects to the configured afterSignOut URL.
*/
redirectToAfterSignOut: () => void;

/**
* Completes an OAuth or SAML redirection flow started by
* {@link Clerk.client.signIn.authenticateWithRedirect} or {@link Clerk.client.signUp.authenticateWithRedirect}
Expand Down Expand Up @@ -435,17 +449,7 @@ export interface Clerk {
handleUnauthenticated: () => Promise<unknown>;
}

export type HandleOAuthCallbackParams = {
/**
* Full URL or path to navigate after successful sign up.
*/
afterSignUpUrl?: string | null;

/**
* Full URL or path to navigate after successful sign in.
*/
afterSignInUrl?: string | null;

export type HandleOAuthCallbackParams = AfterActionURLs & {
/**
* Full URL or path to navigate after successful sign in
* or sign up.
Expand Down Expand Up @@ -513,35 +517,34 @@ type ClerkOptionsNavigation = ClerkOptionsNavigationFn & {
routerDebug?: boolean;
};

export type ClerkOptions = ClerkOptionsNavigation & {
appearance?: Appearance;
localization?: LocalizationResource;
polling?: boolean;
selectInitialSession?: (client: ClientResource) => ActiveSessionResource | null;
/** Controls if ClerkJS will load with the standard browser setup using Clerk cookies */
standardBrowser?: boolean;
/** Optional support email for display in authentication screens */
supportEmail?: string;
touchSession?: boolean;
signInUrl?: string;
signUpUrl?: string;
afterSignInUrl?: string;
afterSignUpUrl?: string;
allowedRedirectOrigins?: Array<string | RegExp>;
isSatellite?: boolean | ((url: URL) => boolean);

/**
* Telemetry options
*/
telemetry?:
| false
| {
disabled?: boolean;
debug?: boolean;
};
export type ClerkOptions = ClerkOptionsNavigation &
AfterActionURLs & {
appearance?: Appearance;
localization?: LocalizationResource;
polling?: boolean;
selectInitialSession?: (client: ClientResource) => ActiveSessionResource | null;
/** Controls if ClerkJS will load with the standard browser setup using Clerk cookies */
standardBrowser?: boolean;
/** Optional support email for display in authentication screens */
supportEmail?: string;
touchSession?: boolean;
signInUrl?: string;
signUpUrl?: string;
allowedRedirectOrigins?: Array<string | RegExp>;
isSatellite?: boolean | ((url: URL) => boolean);

sdkMetadata?: SDKMetadata;
};
/**
* Telemetry options
*/
telemetry?:
| false
| {
disabled?: boolean;
debug?: boolean;
};

sdkMetadata?: SDKMetadata;
};

export interface NavigateOptions {
replace?: boolean;
Expand Down Expand Up @@ -572,7 +575,7 @@ export type SignUpInitialValues = {
username?: string;
};

export type RedirectOptions = {
type AfterActionURLs = {
/**
* Full URL or path to navigate after successful sign in.
*/
Expand All @@ -584,6 +587,13 @@ export type RedirectOptions = {
*/
afterSignUpUrl?: string | null;

/**
* Full URL or path to navigate after successful sign out.
*/
afterSignOutUrl?: string | null;
};

export type RedirectOptions = AfterActionURLs & {
/**
* Full URL or path to navigate after successful sign in,
* or sign up.
Expand Down