Skip to content

Commit f875463

Browse files
authoredOct 29, 2024··
fix(clerk-js): Extend client cookie lifetime (#4312)
1 parent 17c5990 commit f875463

27 files changed

+260
-88
lines changed
 

‎.changeset/eighty-guests-change.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@clerk/clerk-js": minor
3+
"@clerk/types": minor
4+
"@clerk/elements": patch
5+
"@clerk/clerk-react": patch
6+
---
7+
8+
- Introduce `redirectUrl` property on `setActive` as a replacement for `beforeEmit`.
9+
- Deprecates `beforeEmit` property on `setActive`.

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

+105-20
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,65 @@ describe('Clerk singleton', () => {
366366
});
367367
});
368368

369+
it('redirects the user to the /v1/client/touch endpoint if the cookie_expires_at is less than 8 days away', async () => {
370+
mockSession.touch.mockReturnValue(Promise.resolve());
371+
mockClientFetch.mockReturnValue(
372+
Promise.resolve({
373+
activeSessions: [mockSession],
374+
cookieExpiresAt: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now
375+
isEligibleForTouch: () => true,
376+
buildTouchUrl: () =>
377+
`https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`,
378+
}),
379+
);
380+
381+
const sut = new Clerk(productionPublishableKey);
382+
sut.navigate = jest.fn();
383+
await sut.load();
384+
await sut.setActive({ session: mockSession as any as ActiveSessionResource, redirectUrl: '/redirect-url-path' });
385+
const redirectUrl = new URL((sut.navigate as jest.Mock).mock.calls[0]);
386+
expect(redirectUrl.pathname).toEqual('/v1/client/touch');
387+
expect(redirectUrl.searchParams.get('redirect_url')).toEqual(`${mockWindowLocation.href}/redirect-url-path`);
388+
});
389+
390+
it('does not redirect the user to the /v1/client/touch endpoint if the cookie_expires_at is more than 8 days away', async () => {
391+
mockSession.touch.mockReturnValue(Promise.resolve());
392+
mockClientFetch.mockReturnValue(
393+
Promise.resolve({
394+
activeSessions: [mockSession],
395+
cookieExpiresAt: new Date(Date.now() + 10 * 24 * 60 * 60 * 1000), // 10 days from now
396+
isEligibleForTouch: () => false,
397+
buildTouchUrl: () =>
398+
`https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`,
399+
}),
400+
);
401+
402+
const sut = new Clerk(productionPublishableKey);
403+
sut.navigate = jest.fn();
404+
await sut.load();
405+
await sut.setActive({ session: mockSession as any as ActiveSessionResource, redirectUrl: '/redirect-url-path' });
406+
expect(sut.navigate).toHaveBeenCalledWith('/redirect-url-path');
407+
});
408+
409+
it('does not redirect the user to the /v1/client/touch endpoint if the cookie_expires_at is not set', async () => {
410+
mockSession.touch.mockReturnValue(Promise.resolve());
411+
mockClientFetch.mockReturnValue(
412+
Promise.resolve({
413+
activeSessions: [mockSession],
414+
cookieExpiresAt: null,
415+
isEligibleForTouch: () => false,
416+
buildTouchUrl: () =>
417+
`https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`,
418+
}),
419+
);
420+
421+
const sut = new Clerk(productionPublishableKey);
422+
sut.navigate = jest.fn();
423+
await sut.load();
424+
await sut.setActive({ session: mockSession as any as ActiveSessionResource, redirectUrl: '/redirect-url-path' });
425+
expect(sut.navigate).toHaveBeenCalledWith('/redirect-url-path');
426+
});
427+
369428
mockNativeRuntime(() => {
370429
it('calls session.touch in a non-standard browser', async () => {
371430
mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] }));
@@ -496,6 +555,7 @@ describe('Clerk singleton', () => {
496555
expect(sut.setActive).toHaveBeenCalledWith({
497556
session: null,
498557
beforeEmit: expect.any(Function),
558+
redirectUrl: '/',
499559
});
500560
});
501561
});
@@ -518,7 +578,11 @@ describe('Clerk singleton', () => {
518578
expect(mockClientDestroy).not.toHaveBeenCalled();
519579
expect(mockClientRemoveSessions).toHaveBeenCalled();
520580
expect(mockSession1.remove).not.toHaveBeenCalled();
521-
expect(sut.setActive).toHaveBeenCalledWith({ session: null, beforeEmit: expect.any(Function) });
581+
expect(sut.setActive).toHaveBeenCalledWith({
582+
session: null,
583+
beforeEmit: expect.any(Function),
584+
redirectUrl: '/',
585+
});
522586
});
523587
});
524588

@@ -561,7 +625,11 @@ describe('Clerk singleton', () => {
561625
await waitFor(() => {
562626
expect(mockSession1.remove).toHaveBeenCalled();
563627
expect(mockClientDestroy).not.toHaveBeenCalled();
564-
expect(sut.setActive).toHaveBeenCalledWith({ session: null, beforeEmit: expect.any(Function) });
628+
expect(sut.setActive).toHaveBeenCalledWith({
629+
session: null,
630+
beforeEmit: expect.any(Function),
631+
redirectUrl: '/',
632+
});
565633
});
566634
});
567635

@@ -582,7 +650,11 @@ describe('Clerk singleton', () => {
582650
await waitFor(() => {
583651
expect(mockSession1.remove).toHaveBeenCalled();
584652
expect(mockClientDestroy).not.toHaveBeenCalled();
585-
expect(sut.setActive).toHaveBeenCalledWith({ session: null, beforeEmit: expect.any(Function) });
653+
expect(sut.setActive).toHaveBeenCalledWith({
654+
session: null,
655+
beforeEmit: expect.any(Function),
656+
redirectUrl: '/after-sign-out',
657+
});
586658
expect(sut.navigate).toHaveBeenCalledWith('/after-sign-out');
587659
});
588660
});
@@ -1096,6 +1168,16 @@ describe('Clerk singleton', () => {
10961168
});
10971169

10981170
it('redirects the user to the signInForceRedirectUrl if one was provided', async () => {
1171+
const sessionId = 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe';
1172+
const mockSession = {
1173+
id: sessionId,
1174+
remove: jest.fn(),
1175+
status: 'active',
1176+
user: {},
1177+
touch: jest.fn(() => Promise.resolve()),
1178+
getToken: jest.fn(),
1179+
lastActiveToken: { getRawString: () => 'mocked-token' },
1180+
};
10991181
mockEnvironmentFetch.mockReturnValue(
11001182
Promise.resolve({
11011183
authConfig: {},
@@ -1110,6 +1192,7 @@ describe('Clerk singleton', () => {
11101192

11111193
mockClientFetch.mockReturnValue(
11121194
Promise.resolve({
1195+
sessions: [mockSession],
11131196
activeSessions: [],
11141197
signIn: new SignIn(null),
11151198
signUp: new SignUp({
@@ -1124,24 +1207,19 @@ describe('Clerk singleton', () => {
11241207
long_message: "You're already signed in",
11251208
message: "You're already signed in",
11261209
meta: {
1127-
session_id: 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe',
1210+
session_id: sessionId,
11281211
},
11291212
},
11301213
},
11311214
},
11321215
} as any as SignUpJSON),
1216+
isEligibleForTouch: () => false,
11331217
}),
11341218
);
11351219

1136-
const mockSetActive = jest.fn(async (setActiveOpts: any) => {
1137-
await setActiveOpts.beforeEmit();
1138-
});
1139-
11401220
const sut = new Clerk(productionPublishableKey);
11411221
await sut.load(mockedLoadOptions);
1142-
sut.setActive = mockSetActive as any;
1143-
1144-
sut.handleRedirectCallback({
1222+
await sut.handleRedirectCallback({
11451223
signInForceRedirectUrl: '/custom-sign-in',
11461224
});
11471225

@@ -1151,6 +1229,16 @@ describe('Clerk singleton', () => {
11511229
});
11521230

11531231
it('gives priority to signInForceRedirectUrl if signInForceRedirectUrl and signInFallbackRedirectUrl were provided ', async () => {
1232+
const sessionId = 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe';
1233+
const mockSession = {
1234+
id: sessionId,
1235+
remove: jest.fn(),
1236+
status: 'active',
1237+
user: {},
1238+
touch: jest.fn(() => Promise.resolve()),
1239+
getToken: jest.fn(),
1240+
lastActiveToken: { getRawString: () => 'mocked-token' },
1241+
};
11541242
mockEnvironmentFetch.mockReturnValue(
11551243
Promise.resolve({
11561244
authConfig: {},
@@ -1165,6 +1253,7 @@ describe('Clerk singleton', () => {
11651253

11661254
mockClientFetch.mockReturnValue(
11671255
Promise.resolve({
1256+
sessions: [mockSession],
11681257
activeSessions: [],
11691258
signIn: new SignIn(null),
11701259
signUp: new SignUp({
@@ -1179,24 +1268,20 @@ describe('Clerk singleton', () => {
11791268
long_message: "You're already signed in",
11801269
message: "You're already signed in",
11811270
meta: {
1182-
session_id: 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe',
1271+
session_id: sessionId,
11831272
},
11841273
},
11851274
},
11861275
},
11871276
} as any as SignUpJSON),
1277+
isEligibleForTouch: () => false,
11881278
}),
11891279
);
11901280

1191-
const mockSetActive = jest.fn(async (setActiveOpts: any) => {
1192-
await setActiveOpts.beforeEmit();
1193-
});
1194-
11951281
const sut = new Clerk(productionPublishableKey);
11961282
await sut.load(mockedLoadOptions);
1197-
sut.setActive = mockSetActive as any;
11981283

1199-
sut.handleRedirectCallback({
1284+
await sut.handleRedirectCallback({
12001285
signInForceRedirectUrl: '/custom-sign-in',
12011286
signInFallbackRedirectUrl: '/redirect-to',
12021287
} as any);
@@ -1654,7 +1739,7 @@ describe('Clerk singleton', () => {
16541739
await waitFor(() => {
16551740
expect(mockSetActive).toHaveBeenCalledWith({
16561741
session: createdSessionId,
1657-
beforeEmit: expect.any(Function),
1742+
redirectUrl: redirectUrlComplete,
16581743
});
16591744
});
16601745
});
@@ -1714,7 +1799,7 @@ describe('Clerk singleton', () => {
17141799
await waitFor(() => {
17151800
expect(mockSetActive).toHaveBeenCalledWith({
17161801
session: createdSessionId,
1717-
beforeEmit: expect.any(Function),
1802+
redirectUrl: redirectUrlComplete,
17181803
});
17191804
});
17201805
});

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

+27-16
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
addClerkPrefix,
33
ClerkRuntimeError,
4+
deprecated,
45
handleValueOrFn,
56
inBrowser as inClientSide,
67
is4xxError,
@@ -354,6 +355,7 @@ export class Clerk implements ClerkInterface {
354355
return this.setActive({
355356
session: null,
356357
beforeEmit: ignoreEventValue(cb),
358+
redirectUrl,
357359
});
358360
}
359361

@@ -364,6 +366,7 @@ export class Clerk implements ClerkInterface {
364366
return this.setActive({
365367
session: null,
366368
beforeEmit: ignoreEventValue(cb),
369+
redirectUrl,
367370
});
368371
}
369372
};
@@ -746,7 +749,7 @@ export class Clerk implements ClerkInterface {
746749
/**
747750
* `setActive` can be used to set the active session and/or organization.
748751
*/
749-
public setActive = async ({ session, organization, beforeEmit }: SetActiveParams): Promise<void> => {
752+
public setActive = async ({ session, organization, beforeEmit, redirectUrl }: SetActiveParams): Promise<void> => {
750753
if (!this.client) {
751754
throw new Error('setActive is being called before the client is loaded. Wait for init.');
752755
}
@@ -831,10 +834,26 @@ export class Clerk implements ClerkInterface {
831834
if (beforeEmit) {
832835
beforeUnloadTracker?.startTracking();
833836
this.#setTransitiveState();
837+
deprecated('beforeEmit', 'Use the `redirectUrl` property instead.');
834838
await beforeEmit(newSession);
835839
beforeUnloadTracker?.stopTracking();
836840
}
837841

842+
if (redirectUrl) {
843+
beforeUnloadTracker?.startTracking();
844+
this.#setTransitiveState();
845+
846+
if (this.client.isEligibleForTouch()) {
847+
const absoluteRedirectUrl = new URL(redirectUrl, window.location.href);
848+
849+
await this.navigate(this.buildUrlWithAuth(this.client.buildTouchUrl({ redirectUrl: absoluteRedirectUrl })));
850+
} else {
851+
await this.navigate(redirectUrl);
852+
}
853+
854+
beforeUnloadTracker?.stopTracking();
855+
}
856+
838857
//3. Check if hard reloading (onbeforeunload). If not, set the user/session and emit
839858
if (beforeUnloadTracker?.isUnloading()) {
840859
return;
@@ -1105,13 +1124,12 @@ export class Clerk implements ClerkInterface {
11051124
const navigate = (to: string) =>
11061125
customNavigate && typeof customNavigate === 'function' ? customNavigate(to) : this.navigate(to);
11071126

1108-
const redirectComplete = params.redirectUrlComplete ? () => navigate(params.redirectUrlComplete as string) : noop;
11091127
const redirectContinue = params.redirectUrl ? () => navigate(params.redirectUrl as string) : noop;
11101128

11111129
if (shouldCompleteOnThisDevice) {
11121130
return this.setActive({
11131131
session: newSessionId,
1114-
beforeEmit: redirectComplete,
1132+
redirectUrl: params.redirectUrlComplete,
11151133
});
11161134
} else if (shouldContinueOnThisDevice) {
11171135
return redirectContinue();
@@ -1206,8 +1224,6 @@ export class Clerk implements ClerkInterface {
12061224
);
12071225

12081226
const redirectUrls = new RedirectUrls(this.#options, params);
1209-
const navigateAfterSignIn = makeNavigate(redirectUrls.getAfterSignInUrl());
1210-
const navigateAfterSignUp = makeNavigate(redirectUrls.getAfterSignUpUrl());
12111227

12121228
const navigateToContinueSignUp = makeNavigate(
12131229
params.continueSignUpUrl ||
@@ -1240,7 +1256,7 @@ export class Clerk implements ClerkInterface {
12401256
if (si.status === 'complete') {
12411257
return this.setActive({
12421258
session: si.sessionId,
1243-
beforeEmit: navigateAfterSignIn,
1259+
redirectUrl: redirectUrls.getAfterSignInUrl(),
12441260
});
12451261
}
12461262

@@ -1253,7 +1269,7 @@ export class Clerk implements ClerkInterface {
12531269
case 'complete':
12541270
return this.setActive({
12551271
session: res.createdSessionId,
1256-
beforeEmit: navigateAfterSignIn,
1272+
redirectUrl: redirectUrls.getAfterSignInUrl(),
12571273
});
12581274
case 'needs_first_factor':
12591275
return navigateToFactorOne();
@@ -1301,7 +1317,7 @@ export class Clerk implements ClerkInterface {
13011317
case 'complete':
13021318
return this.setActive({
13031319
session: res.createdSessionId,
1304-
beforeEmit: navigateAfterSignUp,
1320+
redirectUrl: redirectUrls.getAfterSignUpUrl(),
13051321
});
13061322
case 'missing_requirements':
13071323
return navigateToNextStepSignUp({ missingFields: res.missingFields });
@@ -1313,7 +1329,7 @@ export class Clerk implements ClerkInterface {
13131329
if (su.status === 'complete') {
13141330
return this.setActive({
13151331
session: su.sessionId,
1316-
beforeEmit: navigateAfterSignUp,
1332+
redirectUrl: redirectUrls.getAfterSignUpUrl(),
13171333
});
13181334
}
13191335

@@ -1337,7 +1353,7 @@ export class Clerk implements ClerkInterface {
13371353
if (sessionId) {
13381354
return this.setActive({
13391355
session: sessionId,
1340-
beforeEmit: navigateAfterSignIn,
1356+
redirectUrl: redirectUrls.getAfterSignInUrl(),
13411357
});
13421358
}
13431359
}
@@ -1462,12 +1478,7 @@ export class Clerk implements ClerkInterface {
14621478
if (signInOrSignUp.createdSessionId) {
14631479
await this.setActive({
14641480
session: signInOrSignUp.createdSessionId,
1465-
beforeEmit: () => {
1466-
if (redirectUrl) {
1467-
return navigate(redirectUrl);
1468-
}
1469-
return Promise.resolve();
1470-
},
1481+
redirectUrl,
14711482
});
14721483
}
14731484
};

‎packages/clerk-js/src/core/resources/Client.ts

+19
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export class Client extends BaseResource implements ClientResource {
1313
signUp: SignUpResource = new SignUp();
1414
signIn: SignInResource = new SignIn();
1515
lastActiveSessionId: string | null = null;
16+
cookieExpiresAt: Date | null = null;
1617
createdAt: Date | null = null;
1718
updatedAt: Date | null = null;
1819

@@ -61,6 +62,7 @@ export class Client extends BaseResource implements ClientResource {
6162
this.signUp = new SignUp(null);
6263
this.signIn = new SignIn(null);
6364
this.lastActiveSessionId = null;
65+
this.cookieExpiresAt = null;
6466
this.createdAt = null;
6567
this.updatedAt = null;
6668
});
@@ -76,13 +78,30 @@ export class Client extends BaseResource implements ClientResource {
7678
return this.sessions.forEach(s => s.clearCache());
7779
}
7880

81+
// isEligibleForTouch returns true if the client cookie is due to expire in 8 days or less
82+
isEligibleForTouch(): boolean {
83+
return !!this.cookieExpiresAt && this.cookieExpiresAt.getTime() - Date.now() <= 8 * 24 * 60 * 60 * 1000; // 8 days
84+
}
85+
86+
buildTouchUrl({ redirectUrl }: { redirectUrl: URL }) {
87+
return BaseResource.fapiClient
88+
.buildUrl({
89+
method: 'GET',
90+
path: '/client/touch',
91+
pathPrefix: 'v1',
92+
search: { redirect_url: redirectUrl.toString() },
93+
})
94+
.toString();
95+
}
96+
7997
fromJSON(data: ClientJSON | null): this {
8098
if (data) {
8199
this.id = data.id;
82100
this.sessions = (data.sessions || []).map(s => new Session(s));
83101
this.signUp = new SignUp(data.sign_up);
84102
this.signIn = new SignIn(data.sign_in);
85103
this.lastActiveSessionId = data.last_active_session_id;
104+
this.cookieExpiresAt = data.cookie_expires_at ? unixEpochToDate(data.cookie_expires_at) : null;
86105
this.createdAt = unixEpochToDate(data.created_at);
87106
this.updatedAt = unixEpochToDate(data.updated_at);
88107
}

‎packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx

+5-4
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ export const OrganizationSwitcherPopover = React.forwardRef<HTMLDivElement, Orga
4848
afterCreateOrganizationUrl,
4949
navigateCreateOrganization,
5050
navigateOrganizationProfile,
51-
navigateAfterSelectPersonal,
52-
navigateAfterSelectOrganization,
51+
afterSelectOrganizationUrl,
52+
afterSelectPersonalUrl,
53+
5354
organizationProfileProps,
5455
skipInvitationScreen,
5556
hideSlug,
@@ -72,15 +73,15 @@ export const OrganizationSwitcherPopover = React.forwardRef<HTMLDivElement, Orga
7273
.runAsync(() =>
7374
setActive({
7475
organization,
75-
beforeEmit: () => navigateAfterSelectOrganization(organization),
76+
redirectUrl: afterSelectOrganizationUrl(organization),
7677
}),
7778
)
7879
.then(close);
7980
};
8081

8182
const handlePersonalWorkspaceClicked = () => {
8283
return card
83-
.runAsync(() => setActive({ organization: null, beforeEmit: () => navigateAfterSelectPersonal(user) }))
84+
.runAsync(() => setActive({ organization: null, redirectUrl: afterSelectPersonalUrl(user) }))
8485
.then(close);
8586
};
8687

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ import { useMultisessionActions } from '../UserButton/useMultisessionActions';
1010
const _SignInAccountSwitcher = () => {
1111
const card = useCardState();
1212
const { userProfileUrl } = useEnvironment().displayConfig;
13-
const { navigateAfterSignIn, path: signInPath } = useSignInContext();
13+
const { navigateAfterSignIn, afterSignInUrl, path: signInPath } = useSignInContext();
1414
const { navigateAfterSignOut } = useSignOutContext();
1515
const { handleSignOutAllClicked, handleSessionClicked, activeSessions, handleAddAccountClicked } =
1616
useMultisessionActions({
1717
navigateAfterSignOut,
1818
navigateAfterSwitchSession: navigateAfterSignIn,
19+
afterSignInUrl,
1920
userProfileUrl,
2021
signInUrl: signInPath,
2122
user: undefined,

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export const SignInFactorOneCodeForm = (props: SignInFactorOneCodeFormProps) =>
3232
const signIn = useCoreSignIn();
3333
const card = useCardState();
3434
const { navigate } = useRouter();
35-
const { navigateAfterSignIn } = useSignInContext();
35+
const { afterSignInUrl } = useSignInContext();
3636
const { setActive } = useClerk();
3737
const supportEmail = useSupportEmail();
3838
const clerk = useClerk();
@@ -62,7 +62,7 @@ export const SignInFactorOneCodeForm = (props: SignInFactorOneCodeFormProps) =>
6262

6363
switch (res.status) {
6464
case 'complete':
65-
return setActive({ session: res.createdSessionId, beforeEmit: navigateAfterSignIn });
65+
return setActive({ session: res.createdSessionId, redirectUrl: afterSignInUrl });
6666
case 'needs_second_factor':
6767
return navigate('../factor-two');
6868
case 'needs_new_password':

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const SignInFactorOneEmailLinkCard = (props: SignInFactorOneEmailLinkCard
2727
const signInContext = useSignInContext();
2828
const { signInUrl } = signInContext;
2929
const { navigate } = useRouter();
30-
const { navigateAfterSignIn } = useSignInContext();
30+
const { afterSignInUrl } = useSignInContext();
3131
const { setActive } = useClerk();
3232
const { startEmailLinkFlow, cancelEmailLinkFlow } = useEmailLink(signIn);
3333
const [showVerifyModal, setShowVerifyModal] = React.useState(false);
@@ -73,7 +73,7 @@ export const SignInFactorOneEmailLinkCard = (props: SignInFactorOneEmailLinkCard
7373
if (si.status === 'complete') {
7474
return setActive({
7575
session: si.createdSessionId,
76-
beforeEmit: navigateAfterSignIn,
76+
redirectUrl: afterSignInUrl,
7777
});
7878
} else if (si.status === 'needs_second_factor') {
7979
return navigate('../factor-two');

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export const SignInFactorOnePasswordCard = (props: SignInFactorOnePasswordProps)
4949
const card = useCardState();
5050
const { setActive } = useClerk();
5151
const signIn = useCoreSignIn();
52-
const { navigateAfterSignIn } = useSignInContext();
52+
const { afterSignInUrl } = useSignInContext();
5353
const supportEmail = useSupportEmail();
5454
const passwordControl = usePasswordControl(props);
5555
const { navigate } = useRouter();
@@ -68,7 +68,7 @@ export const SignInFactorOnePasswordCard = (props: SignInFactorOnePasswordProps)
6868
.then(res => {
6969
switch (res.status) {
7070
case 'complete':
71-
return setActive({ session: res.createdSessionId, beforeEmit: navigateAfterSignIn });
71+
return setActive({ session: res.createdSessionId, redirectUrl: afterSignInUrl });
7272
case 'needs_second_factor':
7373
return navigate('../factor-two');
7474
default:

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ type SignInFactorTwoBackupCodeCardProps = {
1919
export const SignInFactorTwoBackupCodeCard = (props: SignInFactorTwoBackupCodeCardProps) => {
2020
const { onShowAlternativeMethodsClicked } = props;
2121
const signIn = useCoreSignIn();
22-
const { navigateAfterSignIn } = useSignInContext();
22+
const { afterSignInUrl } = useSignInContext();
2323
const { setActive } = useClerk();
2424
const { navigate } = useRouter();
2525
const supportEmail = useSupportEmail();
@@ -47,7 +47,7 @@ export const SignInFactorTwoBackupCodeCard = (props: SignInFactorTwoBackupCodeCa
4747
queryParams.set('createdSessionId', res.createdSessionId);
4848
return navigate(`../reset-password-success?${queryParams.toString()}`);
4949
}
50-
return setActive({ session: res.createdSessionId, beforeEmit: navigateAfterSignIn });
50+
return setActive({ session: res.createdSessionId, redirectUrl: afterSignInUrl });
5151
default:
5252
return console.error(clerkInvalidFAPIResponse(res.status, supportEmail));
5353
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ type SignInFactorTwoCodeFormProps = SignInFactorTwoCodeCard & {
3131
export const SignInFactorTwoCodeForm = (props: SignInFactorTwoCodeFormProps) => {
3232
const signIn = useCoreSignIn();
3333
const card = useCardState();
34-
const { navigateAfterSignIn } = useSignInContext();
34+
const { afterSignInUrl } = useSignInContext();
3535
const { setActive } = useClerk();
3636
const { navigate } = useRouter();
3737
const supportEmail = useSupportEmail();
@@ -77,7 +77,7 @@ export const SignInFactorTwoCodeForm = (props: SignInFactorTwoCodeFormProps) =>
7777
queryParams.set('createdSessionId', res.createdSessionId);
7878
return navigate(`../reset-password-success?${queryParams.toString()}`);
7979
}
80-
return setActive({ session: res.createdSessionId, beforeEmit: navigateAfterSignIn });
80+
return setActive({ session: res.createdSessionId, redirectUrl: afterSignInUrl });
8181
default:
8282
return console.error(clerkInvalidFAPIResponse(res.status, supportEmail));
8383
}

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export function _SignInStart(): JSX.Element {
6565
const signIn = useCoreSignIn();
6666
const { navigate } = useRouter();
6767
const ctx = useSignInContext();
68-
const { navigateAfterSignIn, signUpUrl } = ctx;
68+
const { afterSignInUrl, signUpUrl } = ctx;
6969
const supportEmail = useSupportEmail();
7070
const identifierAttributes = useMemo<SignInStartIdentifier[]>(
7171
() => groupIdentifiers(userSettings.enabledFirstFactorIdentifiers),
@@ -186,7 +186,7 @@ export function _SignInStart(): JSX.Element {
186186
removeClerkQueryParam('__clerk_ticket');
187187
return clerk.setActive({
188188
session: res.createdSessionId,
189-
beforeEmit: navigateAfterSignIn,
189+
redirectUrl: afterSignInUrl,
190190
});
191191
default: {
192192
console.error(clerkInvalidFAPIResponse(res.status, supportEmail));
@@ -289,7 +289,7 @@ export function _SignInStart(): JSX.Element {
289289
case 'complete':
290290
return clerk.setActive({
291291
session: res.createdSessionId,
292-
beforeEmit: navigateAfterSignIn,
292+
redirectUrl: afterSignInUrl,
293293
});
294294
default: {
295295
console.error(clerkInvalidFAPIResponse(res.status, supportEmail));
@@ -328,7 +328,7 @@ export function _SignInStart(): JSX.Element {
328328
await signInWithFields(identifierField);
329329
} else if (alreadySignedInError) {
330330
const sid = alreadySignedInError.meta!.sessionId!;
331-
await clerk.setActive({ session: sid, beforeEmit: navigateAfterSignIn });
331+
await clerk.setActive({ session: sid, redirectUrl: afterSignInUrl });
332332
} else {
333333
handleError(e, [identifierField, instantPasswordField], card.setError);
334334
}

‎packages/clerk-js/src/ui/components/SignIn/shared.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ function useHandleAuthenticateWithPasskey(onSecondFactor: () => Promise<unknown>
1414
// @ts-expect-error -- private method for the time being
1515
const { setActive, __internal_navigateWithError } = useClerk();
1616
const supportEmail = useSupportEmail();
17-
const { navigateAfterSignIn } = useSignInContext();
17+
const { afterSignInUrl } = useSignInContext();
1818
const { authenticateWithPasskey } = useCoreSignIn();
1919

2020
useEffect(() => {
@@ -28,7 +28,7 @@ function useHandleAuthenticateWithPasskey(onSecondFactor: () => Promise<unknown>
2828
const res = await authenticateWithPasskey(...args);
2929
switch (res.status) {
3030
case 'complete':
31-
return setActive({ session: res.createdSessionId, beforeEmit: navigateAfterSignIn });
31+
return setActive({ session: res.createdSessionId, redirectUrl: afterSignInUrl });
3232
case 'needs_second_factor':
3333
return onSecondFactor();
3434
default:

‎packages/clerk-js/src/ui/components/SignUp/SignUpContinue.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function _SignUpContinue() {
3131
const { navigate } = useRouter();
3232
const { displayConfig, userSettings } = useEnvironment();
3333
const { attributes } = userSettings;
34-
const { navigateAfterSignUp, signInUrl, unsafeMetadata, initialValues = {} } = useSignUpContext();
34+
const { afterSignUpUrl, signInUrl, unsafeMetadata, initialValues = {} } = useSignUpContext();
3535
const signUp = useCoreSignUp();
3636
const isProgressiveSignUp = userSettings.signUp.progressive;
3737
const [activeCommIdentifierType, setActiveCommIdentifierType] = React.useState<ActiveIdentifier>(
@@ -157,7 +157,7 @@ function _SignUpContinue() {
157157
signUp: res,
158158
verifyEmailPath: './verify-email-address',
159159
verifyPhonePath: './verify-phone-number',
160-
handleComplete: () => clerk.setActive({ session: res.createdSessionId, beforeEmit: navigateAfterSignUp }),
160+
handleComplete: () => clerk.setActive({ session: res.createdSessionId, redirectUrl: afterSignUpUrl }),
161161
navigate,
162162
}),
163163
)

‎packages/clerk-js/src/ui/components/SignUp/SignUpEmailLinkCard.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const SignUpEmailLinkCard = () => {
1717
const { t } = useLocalizations();
1818
const signUp = useCoreSignUp();
1919
const signUpContext = useSignUpContext();
20-
const { navigateAfterSignUp } = signUpContext;
20+
const { afterSignUpUrl } = signUpContext;
2121
const card = useCardState();
2222
const { displayConfig } = useEnvironment();
2323
const { navigate } = useRouter();
@@ -54,7 +54,7 @@ export const SignUpEmailLinkCard = () => {
5454
signUp: su,
5555
verifyEmailPath: '../verify-email-address',
5656
verifyPhonePath: '../verify-phone-number',
57-
handleComplete: () => setActive({ session: su.createdSessionId, beforeEmit: navigateAfterSignUp }),
57+
handleComplete: () => setActive({ session: su.createdSessionId, redirectUrl: afterSignUpUrl }),
5858
navigate,
5959
});
6060
}

‎packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ function _SignUpStart(): JSX.Element {
3939
const { attributes } = userSettings;
4040
const { setActive } = useClerk();
4141
const ctx = useSignUpContext();
42-
const { navigateAfterSignUp, signInUrl, unsafeMetadata } = ctx;
42+
const { afterSignUpUrl, signInUrl, unsafeMetadata } = ctx;
4343
const [activeCommIdentifierType, setActiveCommIdentifierType] = React.useState<ActiveIdentifier>(
4444
getInitialActiveIdentifier(attributes, userSettings.signUp.progressive),
4545
);
@@ -137,7 +137,7 @@ function _SignUpStart(): JSX.Element {
137137
handleComplete: () => {
138138
removeClerkQueryParam('__clerk_ticket');
139139
removeClerkQueryParam('__clerk_invitation_token');
140-
return setActive({ session: signUp.createdSessionId, beforeEmit: navigateAfterSignUp });
140+
return setActive({ session: signUp.createdSessionId, redirectUrl: afterSignUpUrl });
141141
},
142142
navigate,
143143
});
@@ -235,7 +235,7 @@ function _SignUpStart(): JSX.Element {
235235
signUp: res,
236236
verifyEmailPath: 'verify-email-address',
237237
verifyPhonePath: 'verify-phone-number',
238-
handleComplete: () => setActive({ session: res.createdSessionId, beforeEmit: navigateAfterSignUp }),
238+
handleComplete: () => setActive({ session: res.createdSessionId, redirectUrl: afterSignUpUrl }),
239239
navigate,
240240
redirectUrl,
241241
redirectUrlComplete,

‎packages/clerk-js/src/ui/components/SignUp/SignUpVerificationCodeForm.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ type SignInFactorOneCodeFormProps = {
1919
};
2020

2121
export const SignUpVerificationCodeForm = (props: SignInFactorOneCodeFormProps) => {
22-
const { navigateAfterSignUp } = useSignUpContext();
22+
const { afterSignUpUrl } = useSignUpContext();
2323
const { setActive } = useClerk();
2424
const { navigate } = useRouter();
2525

@@ -36,7 +36,7 @@ export const SignUpVerificationCodeForm = (props: SignInFactorOneCodeFormProps)
3636
signUp: res,
3737
verifyEmailPath: '../verify-email-address',
3838
verifyPhonePath: '../verify-phone-number',
39-
handleComplete: () => setActive({ session: res.createdSessionId, beforeEmit: navigateAfterSignUp }),
39+
handleComplete: () => setActive({ session: res.createdSessionId, redirectUrl: afterSignUpUrl }),
4040
navigate,
4141
});
4242
})

‎packages/clerk-js/src/ui/components/UserButton/useMultisessionActions.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type UseMultisessionActionsParams = {
1313
navigateAfterSignOut?: () => any;
1414
navigateAfterMultiSessionSingleSignOut?: () => any;
1515
navigateAfterSwitchSession?: () => any;
16+
afterSignInUrl?: string;
1617
userProfileUrl?: string;
1718
signInUrl?: string;
1819
} & Pick<UserButtonProps, 'userProfileMode' | 'appearance' | 'userProfileProps'>;
@@ -68,7 +69,7 @@ export const useMultisessionActions = (opts: UseMultisessionActionsParams) => {
6869

6970
const handleSessionClicked = (session: ActiveSessionResource) => async () => {
7071
card.setLoading();
71-
return setActive({ session, beforeEmit: opts.navigateAfterSwitchSession }).finally(() => {
72+
return setActive({ session, redirectUrl: opts.afterSignInUrl }).finally(() => {
7273
card.setIdle();
7374
opts.actionCompleteCallback?.();
7475
});

‎packages/clerk-js/src/ui/components/UserProfile/DeleteUserForm.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ type DeleteUserFormProps = FormProps;
1212
export const DeleteUserForm = withCardStateProvider((props: DeleteUserFormProps) => {
1313
const { onReset } = props;
1414
const card = useCardState();
15-
const { navigateAfterSignOut, navigateAfterMultiSessionSingleSignOutUrl } = useSignOutContext();
15+
const { afterSignOutUrl, afterMultiSessionSingleSignOutUrl } = useSignOutContext();
1616
const { user } = useUser();
1717
const { t } = useLocalizations();
1818
const { otherSessions } = useMultipleSessions({ user });
@@ -41,11 +41,11 @@ export const DeleteUserForm = withCardStateProvider((props: DeleteUserFormProps)
4141
}
4242

4343
await handleAssurance(user.delete);
44-
const navigationCallback =
45-
otherSessions.length === 0 ? navigateAfterSignOut : navigateAfterMultiSessionSingleSignOutUrl;
44+
const redirectUrl = otherSessions.length === 0 ? afterSignOutUrl : afterMultiSessionSingleSignOutUrl;
45+
4646
return await setActive({
4747
session: null,
48-
beforeEmit: navigationCallback,
48+
redirectUrl,
4949
});
5050
} catch (e) {
5151
handleError(e, [], card.setError);

‎packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx

+38-6
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,8 @@ export const useSignInContext = (): SignInContextType => {
194194
export type SignOutContextType = {
195195
navigateAfterSignOut: () => any;
196196
navigateAfterMultiSessionSingleSignOutUrl: () => any;
197+
afterSignOutUrl: string;
198+
afterMultiSessionSingleSignOutUrl: string;
197199
};
198200

199201
export const useSignOutContext = (): SignOutContextType => {
@@ -203,7 +205,12 @@ export const useSignOutContext = (): SignOutContextType => {
203205
const navigateAfterSignOut = () => navigate(clerk.buildAfterSignOutUrl());
204206
const navigateAfterMultiSessionSingleSignOutUrl = () => navigate(clerk.buildAfterMultiSessionSingleSignOutUrl());
205207

206-
return { navigateAfterSignOut, navigateAfterMultiSessionSingleSignOutUrl };
208+
return {
209+
navigateAfterSignOut,
210+
navigateAfterMultiSessionSingleSignOutUrl,
211+
afterSignOutUrl: clerk.buildAfterSignOutUrl(),
212+
afterMultiSessionSingleSignOutUrl: clerk.buildAfterMultiSessionSingleSignOutUrl(),
213+
};
207214
};
208215

209216
type PagesType = {
@@ -334,39 +341,62 @@ export const useOrganizationSwitcherContext = () => {
334341
}: {
335342
organization?: OrganizationResource;
336343
user?: UserResource;
344+
}) => {
345+
const redirectUrl = getAfterSelectOrganizationOrPersonalUrl({
346+
organization,
347+
user,
348+
});
349+
350+
if (redirectUrl) {
351+
return navigate(redirectUrl);
352+
}
353+
354+
return Promise.resolve();
355+
};
356+
357+
const getAfterSelectOrganizationOrPersonalUrl = ({
358+
organization,
359+
user,
360+
}: {
361+
organization?: OrganizationResource;
362+
user?: UserResource;
337363
}) => {
338364
if (typeof ctx.afterSelectPersonalUrl === 'function' && user) {
339-
return navigate(ctx.afterSelectPersonalUrl(user));
365+
return ctx.afterSelectPersonalUrl(user);
340366
}
341367

342368
if (typeof ctx.afterSelectOrganizationUrl === 'function' && organization) {
343-
return navigate(ctx.afterSelectOrganizationUrl(organization));
369+
return ctx.afterSelectOrganizationUrl(organization);
344370
}
345371

346372
if (ctx.afterSelectPersonalUrl && user) {
347373
const parsedUrl = populateParamFromObject({
348374
urlWithParam: ctx.afterSelectPersonalUrl as string,
349375
entity: user,
350376
});
351-
return navigate(parsedUrl);
377+
return parsedUrl;
352378
}
353379

354380
if (ctx.afterSelectOrganizationUrl && organization) {
355381
const parsedUrl = populateParamFromObject({
356382
urlWithParam: ctx.afterSelectOrganizationUrl as string,
357383
entity: organization,
358384
});
359-
return navigate(parsedUrl);
385+
return parsedUrl;
360386
}
361387

362-
return Promise.resolve();
388+
return;
363389
};
364390

365391
const navigateAfterSelectOrganization = (organization: OrganizationResource) =>
366392
navigateAfterSelectOrganizationOrPersonal({ organization });
367393

368394
const navigateAfterSelectPersonal = (user: UserResource) => navigateAfterSelectOrganizationOrPersonal({ user });
369395

396+
const afterSelectOrganizationUrl = (organization: OrganizationResource) =>
397+
getAfterSelectOrganizationOrPersonalUrl({ organization });
398+
const afterSelectPersonalUrl = (user: UserResource) => getAfterSelectOrganizationOrPersonalUrl({ user });
399+
370400
const organizationProfileMode =
371401
!!ctx.organizationProfileUrl && !ctx.organizationProfileMode ? 'navigation' : ctx.organizationProfileMode;
372402

@@ -386,6 +416,8 @@ export const useOrganizationSwitcherContext = () => {
386416
navigateCreateOrganization,
387417
navigateAfterSelectOrganization,
388418
navigateAfterSelectPersonal,
419+
afterSelectOrganizationUrl,
420+
afterSelectPersonalUrl,
389421
componentName,
390422
};
391423
};

‎packages/clerk-js/src/ui/hooks/useSetSessionWithTimeout.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ import { useRouter } from '../router';
77
export const useSetSessionWithTimeout = (delay = 2000) => {
88
const { queryString } = useRouter();
99
const { setActive } = useClerk();
10-
const { navigateAfterSignIn } = useSignInContext();
10+
const { afterSignInUrl } = useSignInContext();
1111

1212
useEffect(() => {
1313
let timeoutId: ReturnType<typeof setTimeout>;
1414
const queryParams = new URLSearchParams(queryString);
1515
const createdSessionId = queryParams.get('createdSessionId');
1616
if (createdSessionId) {
1717
timeoutId = setTimeout(() => {
18-
void setActive({ session: createdSessionId, beforeEmit: navigateAfterSignIn });
18+
void setActive({ session: createdSessionId, redirectUrl: afterSignInUrl });
1919
}, delay);
2020
}
2121

@@ -24,5 +24,5 @@ export const useSetSessionWithTimeout = (delay = 2000) => {
2424
clearTimeout(timeoutId);
2525
}
2626
};
27-
}, [setActive, navigateAfterSignIn, queryString]);
27+
}, [setActive, afterSignInUrl, queryString]);
2828
};

‎packages/elements/src/internals/machines/sign-in/router.machine.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,10 @@ export const SignInRouterMachine = setup({
8787

8888
const session = id || createdSessionId || lastActiveSessionId || null;
8989

90-
const beforeEmit = () =>
91-
context.router?.push(context.router?.searchParams().get('redirect_url') || context.clerk.buildAfterSignInUrl());
92-
void context.clerk.setActive({ session, beforeEmit });
90+
void context.clerk.setActive({
91+
session,
92+
redirectUrl: context.router?.searchParams().get('redirect_url') || context.clerk.buildAfterSignInUrl(),
93+
});
9394

9495
enqueue.raise({ type: 'RESET' }, { delay: 2000 }); // Reset machine after 2s delay.
9596
}),

‎packages/elements/src/internals/machines/sign-up/router.machine.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,10 @@ export const SignUpRouterMachine = setup({
7979
(params?.useLastActiveSession && context.clerk.client.lastActiveSessionId) ||
8080
((event as SignUpRouterNextEvent)?.resource || context.clerk.client.signUp).createdSessionId;
8181

82-
const beforeEmit = () =>
83-
context.router?.push(context.router?.searchParams().get('redirect_url') || context.clerk.buildAfterSignUpUrl());
84-
void context.clerk.setActive({ session, beforeEmit });
82+
void context.clerk.setActive({
83+
session,
84+
redirectUrl: context.router?.searchParams().get('redirect_url') || context.clerk.buildAfterSignUpUrl(),
85+
});
8586
},
8687
delayedReset: raise({ type: 'RESET' }, { delay: 3000 }), // Reset machine after 3s delay.
8788
setError: assign({

‎packages/react/src/isomorphicClerk.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -625,9 +625,9 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
625625
/**
626626
* `setActive` can be used to set the active session and/or organization.
627627
*/
628-
setActive = ({ session, organization, beforeEmit }: SetActiveParams): Promise<void> => {
628+
setActive = ({ session, organization, beforeEmit, redirectUrl }: SetActiveParams): Promise<void> => {
629629
if (this.clerkjs) {
630-
return this.clerkjs.setActive({ session, organization, beforeEmit });
630+
return this.clerkjs.setActive({ session, organization, beforeEmit, redirectUrl });
631631
} else {
632632
return Promise.reject();
633633
}

‎packages/types/src/clerk.ts

+7
Original file line numberDiff line numberDiff line change
@@ -801,10 +801,17 @@ export type SetActiveParams = {
801801
organization?: OrganizationResource | string | null;
802802

803803
/**
804+
* @deprecated use the redirectUrl parameter to redirect a user
805+
*
804806
* Callback run just before the active session and/or organization is set to the passed object.
805807
* Can be used to hook up for pre-navigation actions.
806808
*/
807809
beforeEmit?: BeforeEmitCallback;
810+
811+
/**
812+
* The URL to redirect a user to just before the active session and/or organization is set to the passed object.
813+
*/
814+
redirectUrl?: string;
808815
};
809816

810817
export type SetActive = (params: SetActiveParams) => Promise<void>;

‎packages/types/src/client.ts

+3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ export interface ClientResource extends ClerkResource {
1313
destroy: () => Promise<void>;
1414
removeSessions: () => Promise<ClientResource>;
1515
clearCache: () => void;
16+
isEligibleForTouch: () => boolean;
17+
buildTouchUrl: (params: { redirectUrl: URL }) => string;
1618
lastActiveSessionId: string | null;
19+
cookieExpiresAt: Date | null;
1720
createdAt: Date | null;
1821
updatedAt: Date | null;
1922
}

‎packages/types/src/json.ts

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export interface ClientJSON extends ClerkResourceJSON {
7373
sign_up: SignUpJSON | null;
7474
sign_in: SignInJSON | null;
7575
last_active_session_id: string | null;
76+
cookie_expires_at: number | null;
7677
created_at: number;
7778
updated_at: number;
7879
}

0 commit comments

Comments
 (0)
Please sign in to comment.