From 733c780b13a9e0d5b1f90ea54ddbee1b76c3534e Mon Sep 17 00:00:00 2001 From: issuedat <165281975+issuedat@users.noreply.github.com> Date: Thu, 10 Oct 2024 17:36:42 +0200 Subject: [PATCH 01/15] Extend client cookie expiration --- packages/clerk-js/src/core/clerk.ts | 46 ++++++++++++------- .../clerk-js/src/core/resources/Client.ts | 3 ++ .../SignIn/SignInFactorOneCodeForm.tsx | 4 +- .../SignIn/SignInFactorOneEmailLinkCard.tsx | 4 +- .../SignIn/SignInFactorOnePasswordCard.tsx | 4 +- .../SignIn/SignInFactorTwoBackupCodeCard.tsx | 4 +- .../SignIn/SignInFactorTwoCodeForm.tsx | 4 +- .../src/ui/components/SignIn/SignInStart.tsx | 8 ++-- .../src/ui/components/SignIn/shared.ts | 4 +- .../ui/components/SignUp/SignUpContinue.tsx | 4 +- .../components/SignUp/SignUpEmailLinkCard.tsx | 4 +- .../src/ui/components/SignUp/SignUpStart.tsx | 6 +-- .../SignUp/SignUpVerificationCodeForm.tsx | 4 +- .../src/ui/hooks/useSetSessionWithTimeout.ts | 6 +-- .../machines/sign-in/router.machine.ts | 7 +-- .../machines/sign-up/router.machine.ts | 7 +-- packages/react/src/isomorphicClerk.ts | 4 +- packages/types/src/clerk.ts | 7 +++ packages/types/src/client.ts | 1 + packages/types/src/json.ts | 1 + 20 files changed, 80 insertions(+), 52 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 98ebad1d080..073909fc4da 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -716,7 +716,7 @@ export class Clerk implements ClerkInterface { /** * `setActive` can be used to set the active session and/or organization. */ - public setActive = async ({ session, organization, beforeEmit }: SetActiveParams): Promise => { + public setActive = async ({ session, organization, beforeEmit, redirectUrl }: SetActiveParams): Promise => { if (!this.client) { throw new Error('setActive is being called before the client is loaded. Wait for init.'); } @@ -805,6 +805,28 @@ export class Clerk implements ClerkInterface { beforeUnloadTracker?.stopTracking(); } + if (redirectUrl) { + if ( + this.client.cookieExpiration && + this.client.cookieExpiration.getTime() - Date.now() >= 7 * 24 * 60 * 60 * 1000 // 7 days + ) { + this.navigate( + this.buildUrlWithAuth( + this.#fapiClient + .buildUrl({ + method: 'GET', + path: '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/client/touch', + pathPrefix: 'v1', + search: { redirect_url: redirectUrl }, + }) + .toString(), + ), + ); + } else { + this.navigate(redirectUrl); + } + } + //3. Check if hard reloading (onbeforeunload). If not, set the user/session and emit if (beforeUnloadTracker?.isUnloading()) { return; @@ -1075,13 +1097,12 @@ export class Clerk implements ClerkInterface { const navigate = (to: string) => customNavigate && typeof customNavigate === 'function' ? customNavigate(to) : this.navigate(to); - const redirectComplete = params.redirectUrlComplete ? () => navigate(params.redirectUrlComplete as string) : noop; const redirectContinue = params.redirectUrl ? () => navigate(params.redirectUrl as string) : noop; if (shouldCompleteOnThisDevice) { return this.setActive({ session: newSessionId, - beforeEmit: redirectComplete, + redirectUrl: params.redirectUrlComplete, }); } else if (shouldContinueOnThisDevice) { return redirectContinue(); @@ -1176,8 +1197,6 @@ export class Clerk implements ClerkInterface { ); const redirectUrls = new RedirectUrls(this.#options, params); - const navigateAfterSignIn = makeNavigate(redirectUrls.getAfterSignInUrl()); - const navigateAfterSignUp = makeNavigate(redirectUrls.getAfterSignUpUrl()); const navigateToContinueSignUp = makeNavigate( params.continueSignUpUrl || @@ -1210,7 +1229,7 @@ export class Clerk implements ClerkInterface { if (si.status === 'complete') { return this.setActive({ session: si.sessionId, - beforeEmit: navigateAfterSignIn, + redirectUrl: redirectUrls.getAfterSignInUrl(), }); } @@ -1223,7 +1242,7 @@ export class Clerk implements ClerkInterface { case 'complete': return this.setActive({ session: res.createdSessionId, - beforeEmit: navigateAfterSignIn, + redirectUrl: redirectUrls.getAfterSignInUrl(), }); case 'needs_first_factor': return navigateToFactorOne(); @@ -1271,7 +1290,7 @@ export class Clerk implements ClerkInterface { case 'complete': return this.setActive({ session: res.createdSessionId, - beforeEmit: navigateAfterSignUp, + redirectUrl: redirectUrls.getAfterSignUpUrl(), }); case 'missing_requirements': return navigateToNextStepSignUp({ missingFields: res.missingFields }); @@ -1283,7 +1302,7 @@ export class Clerk implements ClerkInterface { if (su.status === 'complete') { return this.setActive({ session: su.sessionId, - beforeEmit: navigateAfterSignUp, + redirectUrl: redirectUrls.getAfterSignUpUrl(), }); } @@ -1307,7 +1326,7 @@ export class Clerk implements ClerkInterface { if (sessionId) { return this.setActive({ session: sessionId, - beforeEmit: navigateAfterSignIn, + redirectUrl: redirectUrls.getAfterSignInUrl(), }); } } @@ -1429,12 +1448,7 @@ export class Clerk implements ClerkInterface { if (signInOrSignUp.createdSessionId) { await this.setActive({ session: signInOrSignUp.createdSessionId, - beforeEmit: () => { - if (redirectUrl) { - return navigate(redirectUrl); - } - return Promise.resolve(); - }, + redirectUrl, }); } }; diff --git a/packages/clerk-js/src/core/resources/Client.ts b/packages/clerk-js/src/core/resources/Client.ts index 28f0ccc8cc4..d33f830a39f 100644 --- a/packages/clerk-js/src/core/resources/Client.ts +++ b/packages/clerk-js/src/core/resources/Client.ts @@ -13,6 +13,7 @@ export class Client extends BaseResource implements ClientResource { signUp: SignUpResource = new SignUp(); signIn: SignInResource = new SignIn(); lastActiveSessionId: string | null = null; + cookieExpiration: Date | null = null; createdAt: Date | null = null; updatedAt: Date | null = null; @@ -61,6 +62,7 @@ export class Client extends BaseResource implements ClientResource { this.signUp = new SignUp(null); this.signIn = new SignIn(null); this.lastActiveSessionId = null; + this.cookieExpiration = null; this.createdAt = null; this.updatedAt = null; }); @@ -83,6 +85,7 @@ export class Client extends BaseResource implements ClientResource { this.signUp = new SignUp(data.sign_up); this.signIn = new SignIn(data.sign_in); this.lastActiveSessionId = data.last_active_session_id; + this.cookieExpiration = data.cookie_expiration ? unixEpochToDate(data.cookie_expiration) : null; this.createdAt = unixEpochToDate(data.created_at); this.updatedAt = unixEpochToDate(data.updated_at); } diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneCodeForm.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneCodeForm.tsx index fb7f8bdd1e5..523d6657a12 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneCodeForm.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneCodeForm.tsx @@ -32,7 +32,7 @@ export const SignInFactorOneCodeForm = (props: SignInFactorOneCodeFormProps) => const signIn = useCoreSignIn(); const card = useCardState(); const { navigate } = useRouter(); - const { navigateAfterSignIn } = useSignInContext(); + const { afterSignInUrl } = useSignInContext(); const { setActive } = useClerk(); const supportEmail = useSupportEmail(); const clerk = useClerk(); @@ -62,7 +62,7 @@ export const SignInFactorOneCodeForm = (props: SignInFactorOneCodeFormProps) => switch (res.status) { case 'complete': - return setActive({ session: res.createdSessionId, beforeEmit: navigateAfterSignIn }); + return setActive({ session: res.createdSessionId, redirectUrl: afterSignInUrl }); case 'needs_second_factor': return navigate('../factor-two'); case 'needs_new_password': diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneEmailLinkCard.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneEmailLinkCard.tsx index a8f9609733c..f9275a02837 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneEmailLinkCard.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneEmailLinkCard.tsx @@ -27,7 +27,7 @@ export const SignInFactorOneEmailLinkCard = (props: SignInFactorOneEmailLinkCard const signInContext = useSignInContext(); const { signInUrl } = signInContext; const { navigate } = useRouter(); - const { navigateAfterSignIn } = useSignInContext(); + const { afterSignInUrl } = useSignInContext(); const { setActive } = useClerk(); const { startEmailLinkFlow, cancelEmailLinkFlow } = useEmailLink(signIn); const [showVerifyModal, setShowVerifyModal] = React.useState(false); @@ -73,7 +73,7 @@ export const SignInFactorOneEmailLinkCard = (props: SignInFactorOneEmailLinkCard if (si.status === 'complete') { return setActive({ session: si.createdSessionId, - beforeEmit: navigateAfterSignIn, + redirectUrl: afterSignInUrl, }); } else if (si.status === 'needs_second_factor') { return navigate('../factor-two'); diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOnePasswordCard.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOnePasswordCard.tsx index 72db6b6891a..9d79266f12a 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOnePasswordCard.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOnePasswordCard.tsx @@ -49,7 +49,7 @@ export const SignInFactorOnePasswordCard = (props: SignInFactorOnePasswordProps) const card = useCardState(); const { setActive } = useClerk(); const signIn = useCoreSignIn(); - const { navigateAfterSignIn } = useSignInContext(); + const { afterSignInUrl } = useSignInContext(); const supportEmail = useSupportEmail(); const passwordControl = usePasswordControl(props); const { navigate } = useRouter(); @@ -68,7 +68,7 @@ export const SignInFactorOnePasswordCard = (props: SignInFactorOnePasswordProps) .then(res => { switch (res.status) { case 'complete': - return setActive({ session: res.createdSessionId, beforeEmit: navigateAfterSignIn }); + return setActive({ session: res.createdSessionId, redirectUrl: afterSignInUrl }); case 'needs_second_factor': return navigate('../factor-two'); default: diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwoBackupCodeCard.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwoBackupCodeCard.tsx index ac9ba597aa3..4b2f0fce6a8 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwoBackupCodeCard.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwoBackupCodeCard.tsx @@ -19,7 +19,7 @@ type SignInFactorTwoBackupCodeCardProps = { export const SignInFactorTwoBackupCodeCard = (props: SignInFactorTwoBackupCodeCardProps) => { const { onShowAlternativeMethodsClicked } = props; const signIn = useCoreSignIn(); - const { navigateAfterSignIn } = useSignInContext(); + const { afterSignInUrl } = useSignInContext(); const { setActive } = useClerk(); const { navigate } = useRouter(); const supportEmail = useSupportEmail(); @@ -47,7 +47,7 @@ export const SignInFactorTwoBackupCodeCard = (props: SignInFactorTwoBackupCodeCa queryParams.set('createdSessionId', res.createdSessionId); return navigate(`../reset-password-success?${queryParams.toString()}`); } - return setActive({ session: res.createdSessionId, beforeEmit: navigateAfterSignIn }); + return setActive({ session: res.createdSessionId, redirectUrl: afterSignInUrl }); default: return console.error(clerkInvalidFAPIResponse(res.status, supportEmail)); } diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwoCodeForm.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwoCodeForm.tsx index 3cdec7e9021..5002702237d 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwoCodeForm.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwoCodeForm.tsx @@ -31,7 +31,7 @@ type SignInFactorTwoCodeFormProps = SignInFactorTwoCodeCard & { export const SignInFactorTwoCodeForm = (props: SignInFactorTwoCodeFormProps) => { const signIn = useCoreSignIn(); const card = useCardState(); - const { navigateAfterSignIn } = useSignInContext(); + const { afterSignInUrl } = useSignInContext(); const { setActive } = useClerk(); const { navigate } = useRouter(); const supportEmail = useSupportEmail(); @@ -77,7 +77,7 @@ export const SignInFactorTwoCodeForm = (props: SignInFactorTwoCodeFormProps) => queryParams.set('createdSessionId', res.createdSessionId); return navigate(`../reset-password-success?${queryParams.toString()}`); } - return setActive({ session: res.createdSessionId, beforeEmit: navigateAfterSignIn }); + return setActive({ session: res.createdSessionId, redirectUrl: afterSignInUrl }); default: return console.error(clerkInvalidFAPIResponse(res.status, supportEmail)); } diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx index 97651aaf661..c7e45789166 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx @@ -65,7 +65,7 @@ export function _SignInStart(): JSX.Element { const signIn = useCoreSignIn(); const { navigate } = useRouter(); const ctx = useSignInContext(); - const { navigateAfterSignIn, signUpUrl } = ctx; + const { afterSignInUrl, signUpUrl } = ctx; const supportEmail = useSupportEmail(); const identifierAttributes = useMemo( () => groupIdentifiers(userSettings.enabledFirstFactorIdentifiers), @@ -186,7 +186,7 @@ export function _SignInStart(): JSX.Element { removeClerkQueryParam('__clerk_ticket'); return clerk.setActive({ session: res.createdSessionId, - beforeEmit: navigateAfterSignIn, + redirectUrl: afterSignInUrl, }); default: { console.error(clerkInvalidFAPIResponse(res.status, supportEmail)); @@ -289,7 +289,7 @@ export function _SignInStart(): JSX.Element { case 'complete': return clerk.setActive({ session: res.createdSessionId, - beforeEmit: navigateAfterSignIn, + redirectUrl: afterSignInUrl, }); default: { console.error(clerkInvalidFAPIResponse(res.status, supportEmail)); @@ -328,7 +328,7 @@ export function _SignInStart(): JSX.Element { await signInWithFields(identifierField); } else if (alreadySignedInError) { const sid = alreadySignedInError.meta!.sessionId!; - await clerk.setActive({ session: sid, beforeEmit: navigateAfterSignIn }); + await clerk.setActive({ session: sid, redirectUrl: afterSignInUrl }); } else { handleError(e, [identifierField, instantPasswordField], card.setError); } diff --git a/packages/clerk-js/src/ui/components/SignIn/shared.ts b/packages/clerk-js/src/ui/components/SignIn/shared.ts index 95a27a25031..0770b6cef51 100644 --- a/packages/clerk-js/src/ui/components/SignIn/shared.ts +++ b/packages/clerk-js/src/ui/components/SignIn/shared.ts @@ -14,7 +14,7 @@ function useHandleAuthenticateWithPasskey(onSecondFactor: () => Promise // @ts-expect-error -- private method for the time being const { setActive, __internal_navigateWithError } = useClerk(); const supportEmail = useSupportEmail(); - const { navigateAfterSignIn } = useSignInContext(); + const { afterSignInUrl } = useSignInContext(); const { authenticateWithPasskey } = useCoreSignIn(); useEffect(() => { @@ -28,7 +28,7 @@ function useHandleAuthenticateWithPasskey(onSecondFactor: () => Promise const res = await authenticateWithPasskey(...args); switch (res.status) { case 'complete': - return setActive({ session: res.createdSessionId, beforeEmit: navigateAfterSignIn }); + return setActive({ session: res.createdSessionId, redirectUrl: afterSignInUrl }); case 'needs_second_factor': return onSecondFactor(); default: diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpContinue.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpContinue.tsx index 7ac0c7188e7..15ba81e33ff 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpContinue.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpContinue.tsx @@ -31,7 +31,7 @@ function _SignUpContinue() { const { navigate } = useRouter(); const { displayConfig, userSettings } = useEnvironment(); const { attributes } = userSettings; - const { navigateAfterSignUp, signInUrl, unsafeMetadata, initialValues = {} } = useSignUpContext(); + const { afterSignUpUrl, signInUrl, unsafeMetadata, initialValues = {} } = useSignUpContext(); const signUp = useCoreSignUp(); const isProgressiveSignUp = userSettings.signUp.progressive; const [activeCommIdentifierType, setActiveCommIdentifierType] = React.useState( @@ -144,7 +144,7 @@ function _SignUpContinue() { signUp: res, verifyEmailPath: './verify-email-address', verifyPhonePath: './verify-phone-number', - handleComplete: () => clerk.setActive({ session: res.createdSessionId, beforeEmit: navigateAfterSignUp }), + handleComplete: () => clerk.setActive({ session: res.createdSessionId, redirectUrl: afterSignUpUrl }), navigate, }), ) diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpEmailLinkCard.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpEmailLinkCard.tsx index fbdd1d4eb2b..a5b701e4c93 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpEmailLinkCard.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpEmailLinkCard.tsx @@ -17,7 +17,7 @@ export const SignUpEmailLinkCard = () => { const { t } = useLocalizations(); const signUp = useCoreSignUp(); const signUpContext = useSignUpContext(); - const { navigateAfterSignUp } = signUpContext; + const { afterSignUpUrl } = signUpContext; const card = useCardState(); const { displayConfig } = useEnvironment(); const { navigate } = useRouter(); @@ -54,7 +54,7 @@ export const SignUpEmailLinkCard = () => { signUp: su, verifyEmailPath: '../verify-email-address', verifyPhonePath: '../verify-phone-number', - handleComplete: () => setActive({ session: su.createdSessionId, beforeEmit: navigateAfterSignUp }), + handleComplete: () => setActive({ session: su.createdSessionId, redirectUrl: afterSignUpUrl }), navigate, }); } diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx index 6a76ca18898..497329ad9ce 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx @@ -37,7 +37,7 @@ function _SignUpStart(): JSX.Element { const { attributes } = userSettings; const { setActive } = useClerk(); const ctx = useSignUpContext(); - const { navigateAfterSignUp, signInUrl, unsafeMetadata } = ctx; + const { afterSignUpUrl, signInUrl, unsafeMetadata } = ctx; const [activeCommIdentifierType, setActiveCommIdentifierType] = React.useState( getInitialActiveIdentifier(attributes, userSettings.signUp.progressive), ); @@ -127,7 +127,7 @@ function _SignUpStart(): JSX.Element { handleComplete: () => { removeClerkQueryParam('__clerk_ticket'); removeClerkQueryParam('__clerk_invitation_token'); - return setActive({ session: signUp.createdSessionId, beforeEmit: navigateAfterSignUp }); + return setActive({ session: signUp.createdSessionId, redirectUrl: afterSignUpUrl }); }, navigate, }); @@ -227,7 +227,7 @@ function _SignUpStart(): JSX.Element { signUp: res, verifyEmailPath: 'verify-email-address', verifyPhonePath: 'verify-phone-number', - handleComplete: () => setActive({ session: res.createdSessionId, beforeEmit: navigateAfterSignUp }), + handleComplete: () => setActive({ session: res.createdSessionId, redirectUrl: afterSignUpUrl }), navigate, redirectUrl, redirectUrlComplete, diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpVerificationCodeForm.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpVerificationCodeForm.tsx index 8ef2f3d5ac7..358ff0fae1f 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpVerificationCodeForm.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpVerificationCodeForm.tsx @@ -19,7 +19,7 @@ type SignInFactorOneCodeFormProps = { }; export const SignUpVerificationCodeForm = (props: SignInFactorOneCodeFormProps) => { - const { navigateAfterSignUp } = useSignUpContext(); + const { afterSignUpUrl } = useSignUpContext(); const { setActive } = useClerk(); const { navigate } = useRouter(); @@ -36,7 +36,7 @@ export const SignUpVerificationCodeForm = (props: SignInFactorOneCodeFormProps) signUp: res, verifyEmailPath: '../verify-email-address', verifyPhonePath: '../verify-phone-number', - handleComplete: () => setActive({ session: res.createdSessionId, beforeEmit: navigateAfterSignUp }), + handleComplete: () => setActive({ session: res.createdSessionId, redirectUrl: afterSignUpUrl }), navigate, }); }) diff --git a/packages/clerk-js/src/ui/hooks/useSetSessionWithTimeout.ts b/packages/clerk-js/src/ui/hooks/useSetSessionWithTimeout.ts index 21eb6fd716e..832ff461637 100644 --- a/packages/clerk-js/src/ui/hooks/useSetSessionWithTimeout.ts +++ b/packages/clerk-js/src/ui/hooks/useSetSessionWithTimeout.ts @@ -7,7 +7,7 @@ import { useRouter } from '../router'; export const useSetSessionWithTimeout = (delay = 2000) => { const { queryString } = useRouter(); const { setActive } = useClerk(); - const { navigateAfterSignIn } = useSignInContext(); + const { afterSignInUrl } = useSignInContext(); useEffect(() => { let timeoutId: ReturnType; @@ -15,7 +15,7 @@ export const useSetSessionWithTimeout = (delay = 2000) => { const createdSessionId = queryParams.get('createdSessionId'); if (createdSessionId) { timeoutId = setTimeout(() => { - void setActive({ session: createdSessionId, beforeEmit: navigateAfterSignIn }); + void setActive({ session: createdSessionId, redirectUrl: afterSignInUrl }); }, delay); } @@ -24,5 +24,5 @@ export const useSetSessionWithTimeout = (delay = 2000) => { clearTimeout(timeoutId); } }; - }, [setActive, navigateAfterSignIn, queryString]); + }, [setActive, afterSignInUrl, queryString]); }; diff --git a/packages/elements/src/internals/machines/sign-in/router.machine.ts b/packages/elements/src/internals/machines/sign-in/router.machine.ts index e179330f4f2..18eac1068f3 100644 --- a/packages/elements/src/internals/machines/sign-in/router.machine.ts +++ b/packages/elements/src/internals/machines/sign-in/router.machine.ts @@ -87,9 +87,10 @@ export const SignInRouterMachine = setup({ const session = id || createdSessionId || lastActiveSessionId || null; - const beforeEmit = () => - context.router?.push(context.router?.searchParams().get('redirect_url') || context.clerk.buildAfterSignInUrl()); - void context.clerk.setActive({ session, beforeEmit }); + void context.clerk.setActive({ + session, + redirectUrl: context.router?.searchParams().get('redirect_url') || context.clerk.buildAfterSignInUrl(), + }); enqueue.raise({ type: 'RESET' }, { delay: 2000 }); // Reset machine after 2s delay. }), diff --git a/packages/elements/src/internals/machines/sign-up/router.machine.ts b/packages/elements/src/internals/machines/sign-up/router.machine.ts index 5019bd03dc7..0c43d4705b6 100644 --- a/packages/elements/src/internals/machines/sign-up/router.machine.ts +++ b/packages/elements/src/internals/machines/sign-up/router.machine.ts @@ -78,9 +78,10 @@ export const SignUpRouterMachine = setup({ (params?.useLastActiveSession && context.clerk.client.lastActiveSessionId) || ((event as SignUpRouterNextEvent)?.resource || context.clerk.client.signUp).createdSessionId; - const beforeEmit = () => - context.router?.push(context.router?.searchParams().get('redirect_url') || context.clerk.buildAfterSignUpUrl()); - void context.clerk.setActive({ session, beforeEmit }); + void context.clerk.setActive({ + session, + redirectUrl: context.router?.searchParams().get('redirect_url') || context.clerk.buildAfterSignUpUrl(), + }); }, delayedReset: raise({ type: 'RESET' }, { delay: 3000 }), // Reset machine after 3s delay. setError: assign({ diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 1623b81cadb..3e7d1419068 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -625,9 +625,9 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { /** * `setActive` can be used to set the active session and/or organization. */ - setActive = ({ session, organization, beforeEmit }: SetActiveParams): Promise => { + setActive = ({ session, organization, beforeEmit, redirectUrl }: SetActiveParams): Promise => { if (this.clerkjs) { - return this.clerkjs.setActive({ session, organization, beforeEmit }); + return this.clerkjs.setActive({ session, organization, beforeEmit, redirectUrl }); } else { return Promise.reject(); } diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 3f1b8abd9e2..e4f4b5c015d 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -787,10 +787,17 @@ export type SetActiveParams = { organization?: OrganizationResource | string | null; /** + * @deprecated use the redirectUrl parameter to redirect a user + * * Callback run just before the active session and/or organization is set to the passed object. * Can be used to hook up for pre-navigation actions. */ beforeEmit?: BeforeEmitCallback; + + /** + * The URL to redirect a user to just before the active session and/or organization is set to the passed object. + */ + redirectUrl?: string; }; export type SetActive = (params: SetActiveParams) => Promise; diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index 4326520dd0a..575a4ff1fbc 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -14,6 +14,7 @@ export interface ClientResource extends ClerkResource { removeSessions: () => Promise; clearCache: () => void; lastActiveSessionId: string | null; + cookieExpiration: Date | null; createdAt: Date | null; updatedAt: Date | null; } diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts index 88a07f00770..c27baa0d231 100644 --- a/packages/types/src/json.ts +++ b/packages/types/src/json.ts @@ -73,6 +73,7 @@ export interface ClientJSON extends ClerkResourceJSON { sign_up: SignUpJSON | null; sign_in: SignInJSON | null; last_active_session_id: string | null; + cookie_expiration: number | null; created_at: number; updated_at: number; } From 37fdb7d2d6476fac8ef9454462d5fd32e67e65ec Mon Sep 17 00:00:00 2001 From: issuedat <165281975+issuedat@users.noreply.github.com> Date: Fri, 18 Oct 2024 16:15:38 +0200 Subject: [PATCH 02/15] chore: replace internal beforeEmit calls with redirectUrl --- packages/clerk-js/src/core/clerk.ts | 7 ++- .../clerk-js/src/core/resources/Client.ts | 6 +-- .../OrganizationSwitcherPopover.tsx | 9 ++-- .../SignIn/SignInAccountSwitcher.tsx | 3 +- .../UserButton/useMultisessionActions.tsx | 3 +- .../components/UserProfile/DeleteUserForm.tsx | 8 ++-- .../ui/contexts/ClerkUIComponentsContext.tsx | 44 ++++++++++++++++--- packages/types/src/client.ts | 2 +- packages/types/src/json.ts | 2 +- 9 files changed, 61 insertions(+), 23 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index d2dd6809b33..6d443f1afc3 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -354,6 +354,7 @@ export class Clerk implements ClerkInterface { return this.setActive({ session: null, beforeEmit: ignoreEventValue(cb), + redirectUrl, }); } @@ -364,6 +365,7 @@ export class Clerk implements ClerkInterface { return this.setActive({ session: null, beforeEmit: ignoreEventValue(cb), + redirectUrl, }); } }; @@ -837,9 +839,10 @@ export class Clerk implements ClerkInterface { if (redirectUrl) { if ( - this.client.cookieExpiration && - this.client.cookieExpiration.getTime() - Date.now() >= 7 * 24 * 60 * 60 * 1000 // 7 days + this.client.cookieExpiresAt && + this.client.cookieExpiresAt.getTime() - Date.now() <= 8 * 24 * 60 * 60 * 1000 // 8 days ) { + console.log('REDIRECTING TO: /v1/client/touch?redirect_url=' + redirectUrl); this.navigate( this.buildUrlWithAuth( this.#fapiClient diff --git a/packages/clerk-js/src/core/resources/Client.ts b/packages/clerk-js/src/core/resources/Client.ts index d33f830a39f..0336a3308a1 100644 --- a/packages/clerk-js/src/core/resources/Client.ts +++ b/packages/clerk-js/src/core/resources/Client.ts @@ -13,7 +13,7 @@ export class Client extends BaseResource implements ClientResource { signUp: SignUpResource = new SignUp(); signIn: SignInResource = new SignIn(); lastActiveSessionId: string | null = null; - cookieExpiration: Date | null = null; + cookieExpiresAt: Date | null = null; createdAt: Date | null = null; updatedAt: Date | null = null; @@ -62,7 +62,7 @@ export class Client extends BaseResource implements ClientResource { this.signUp = new SignUp(null); this.signIn = new SignIn(null); this.lastActiveSessionId = null; - this.cookieExpiration = null; + this.cookieExpiresAt = null; this.createdAt = null; this.updatedAt = null; }); @@ -85,7 +85,7 @@ export class Client extends BaseResource implements ClientResource { this.signUp = new SignUp(data.sign_up); this.signIn = new SignIn(data.sign_in); this.lastActiveSessionId = data.last_active_session_id; - this.cookieExpiration = data.cookie_expiration ? unixEpochToDate(data.cookie_expiration) : null; + this.cookieExpiresAt = data.cookie_expires_at ? unixEpochToDate(data.cookie_expires_at) : null; this.createdAt = unixEpochToDate(data.created_at); this.updatedAt = unixEpochToDate(data.updated_at); } diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx index 1fbb9436e91..ca64e70c060 100644 --- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx @@ -48,8 +48,9 @@ export const OrganizationSwitcherPopover = React.forwardRef setActive({ organization, - beforeEmit: () => navigateAfterSelectOrganization(organization), + redirectUrl: afterSelectOrganizationUrl(organization), }), ) .then(close); @@ -80,7 +81,7 @@ export const OrganizationSwitcherPopover = React.forwardRef { return card - .runAsync(() => setActive({ organization: null, beforeEmit: () => navigateAfterSelectPersonal(user) })) + .runAsync(() => setActive({ organization: null, redirectUrl: afterSelectPersonalUrl(user) })) .then(close); }; diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx index 78b095b27bc..df1a14e22fb 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx @@ -10,12 +10,13 @@ import { useMultisessionActions } from '../UserButton/useMultisessionActions'; const _SignInAccountSwitcher = () => { const card = useCardState(); const { userProfileUrl } = useEnvironment().displayConfig; - const { navigateAfterSignIn, path: signInPath } = useSignInContext(); + const { navigateAfterSignIn, afterSignInUrl, path: signInPath } = useSignInContext(); const { navigateAfterSignOut } = useSignOutContext(); const { handleSignOutAllClicked, handleSessionClicked, activeSessions, handleAddAccountClicked } = useMultisessionActions({ navigateAfterSignOut, navigateAfterSwitchSession: navigateAfterSignIn, + afterSignInUrl, userProfileUrl, signInUrl: signInPath, user: undefined, diff --git a/packages/clerk-js/src/ui/components/UserButton/useMultisessionActions.tsx b/packages/clerk-js/src/ui/components/UserButton/useMultisessionActions.tsx index 6dfb5a06d0a..cb12f020812 100644 --- a/packages/clerk-js/src/ui/components/UserButton/useMultisessionActions.tsx +++ b/packages/clerk-js/src/ui/components/UserButton/useMultisessionActions.tsx @@ -13,6 +13,7 @@ type UseMultisessionActionsParams = { navigateAfterSignOut?: () => any; navigateAfterMultiSessionSingleSignOut?: () => any; navigateAfterSwitchSession?: () => any; + afterSignInUrl?: string; userProfileUrl?: string; signInUrl?: string; } & Pick; @@ -68,7 +69,7 @@ export const useMultisessionActions = (opts: UseMultisessionActionsParams) => { const handleSessionClicked = (session: ActiveSessionResource) => async () => { card.setLoading(); - return setActive({ session, beforeEmit: opts.navigateAfterSwitchSession }).finally(() => { + return setActive({ session, redirectUrl: opts.afterSignInUrl }).finally(() => { card.setIdle(); opts.actionCompleteCallback?.(); }); diff --git a/packages/clerk-js/src/ui/components/UserProfile/DeleteUserForm.tsx b/packages/clerk-js/src/ui/components/UserProfile/DeleteUserForm.tsx index c83586766e8..75b3de7a288 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/DeleteUserForm.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/DeleteUserForm.tsx @@ -12,7 +12,7 @@ type DeleteUserFormProps = FormProps; export const DeleteUserForm = withCardStateProvider((props: DeleteUserFormProps) => { const { onReset } = props; const card = useCardState(); - const { navigateAfterSignOut, navigateAfterMultiSessionSingleSignOutUrl } = useSignOutContext(); + const { afterSignOutUrl, afterMultiSessionSingleSignOutUrl } = useSignOutContext(); const { user } = useUser(); const { t } = useLocalizations(); const { otherSessions } = useMultipleSessions({ user }); @@ -41,11 +41,11 @@ export const DeleteUserForm = withCardStateProvider((props: DeleteUserFormProps) } await handleAssurance(user.delete); - const navigationCallback = - otherSessions.length === 0 ? navigateAfterSignOut : navigateAfterMultiSessionSingleSignOutUrl; + const redirectUrl = otherSessions.length === 0 ? afterSignOutUrl : afterMultiSessionSingleSignOutUrl; + return await setActive({ session: null, - beforeEmit: navigationCallback, + redirectUrl, }); } catch (e) { handleError(e, [], card.setError); diff --git a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx index c9e0434b434..14a42d66829 100644 --- a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx +++ b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx @@ -194,6 +194,8 @@ export const useSignInContext = (): SignInContextType => { export type SignOutContextType = { navigateAfterSignOut: () => any; navigateAfterMultiSessionSingleSignOutUrl: () => any; + afterSignOutUrl: string; + afterMultiSessionSingleSignOutUrl: string; }; export const useSignOutContext = (): SignOutContextType => { @@ -203,7 +205,12 @@ export const useSignOutContext = (): SignOutContextType => { const navigateAfterSignOut = () => navigate(clerk.buildAfterSignOutUrl()); const navigateAfterMultiSessionSingleSignOutUrl = () => navigate(clerk.buildAfterMultiSessionSingleSignOutUrl()); - return { navigateAfterSignOut, navigateAfterMultiSessionSingleSignOutUrl }; + return { + navigateAfterSignOut, + navigateAfterMultiSessionSingleSignOutUrl, + afterSignOutUrl: clerk.buildAfterSignOutUrl(), + afterMultiSessionSingleSignOutUrl: clerk.buildAfterMultiSessionSingleSignOutUrl(), + }; }; type PagesType = { @@ -334,13 +341,32 @@ export const useOrganizationSwitcherContext = () => { }: { organization?: OrganizationResource; user?: UserResource; + }) => { + const redirectUrl = getAfterSelectOrganizationOrPersonalUrl({ + organization, + user, + }); + + if (redirectUrl) { + return navigate(redirectUrl); + } + + return Promise.resolve(); + }; + + const getAfterSelectOrganizationOrPersonalUrl = ({ + organization, + user, + }: { + organization?: OrganizationResource; + user?: UserResource; }) => { if (typeof ctx.afterSelectPersonalUrl === 'function' && user) { - return navigate(ctx.afterSelectPersonalUrl(user)); + return ctx.afterSelectPersonalUrl(user); } if (typeof ctx.afterSelectOrganizationUrl === 'function' && organization) { - return navigate(ctx.afterSelectOrganizationUrl(organization)); + return ctx.afterSelectOrganizationUrl(organization); } if (ctx.afterSelectPersonalUrl && user) { @@ -348,7 +374,7 @@ export const useOrganizationSwitcherContext = () => { urlWithParam: ctx.afterSelectPersonalUrl as string, entity: user, }); - return navigate(parsedUrl); + return parsedUrl; } if (ctx.afterSelectOrganizationUrl && organization) { @@ -356,10 +382,10 @@ export const useOrganizationSwitcherContext = () => { urlWithParam: ctx.afterSelectOrganizationUrl as string, entity: organization, }); - return navigate(parsedUrl); + return parsedUrl; } - return Promise.resolve(); + return; }; const navigateAfterSelectOrganization = (organization: OrganizationResource) => @@ -367,6 +393,10 @@ export const useOrganizationSwitcherContext = () => { const navigateAfterSelectPersonal = (user: UserResource) => navigateAfterSelectOrganizationOrPersonal({ user }); + const afterSelectOrganizationUrl = (organization: OrganizationResource) => + getAfterSelectOrganizationOrPersonalUrl({ organization }); + const afterSelectPersonalUrl = (user: UserResource) => getAfterSelectOrganizationOrPersonalUrl({ user }); + const organizationProfileMode = !!ctx.organizationProfileUrl && !ctx.organizationProfileMode ? 'navigation' : ctx.organizationProfileMode; @@ -386,6 +416,8 @@ export const useOrganizationSwitcherContext = () => { navigateCreateOrganization, navigateAfterSelectOrganization, navigateAfterSelectPersonal, + afterSelectOrganizationUrl, + afterSelectPersonalUrl, componentName, }; }; diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index 575a4ff1fbc..471035bf3e4 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -14,7 +14,7 @@ export interface ClientResource extends ClerkResource { removeSessions: () => Promise; clearCache: () => void; lastActiveSessionId: string | null; - cookieExpiration: Date | null; + cookieExpiresAt: Date | null; createdAt: Date | null; updatedAt: Date | null; } diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts index b3ec94fee68..4bbd03f9056 100644 --- a/packages/types/src/json.ts +++ b/packages/types/src/json.ts @@ -73,7 +73,7 @@ export interface ClientJSON extends ClerkResourceJSON { sign_up: SignUpJSON | null; sign_in: SignInJSON | null; last_active_session_id: string | null; - cookie_expiration: number | null; + cookie_expires_at: number | null; created_at: number; updated_at: number; } From 8b8e460d7843433f6f36d5eca534b71d44515b5d Mon Sep 17 00:00:00 2001 From: issuedat <165281975+issuedat@users.noreply.github.com> Date: Fri, 18 Oct 2024 16:20:12 +0200 Subject: [PATCH 03/15] chore: remove console.log --- packages/clerk-js/src/core/clerk.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 6d443f1afc3..7b6b5d43036 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -842,7 +842,6 @@ export class Clerk implements ClerkInterface { this.client.cookieExpiresAt && this.client.cookieExpiresAt.getTime() - Date.now() <= 8 * 24 * 60 * 60 * 1000 // 8 days ) { - console.log('REDIRECTING TO: /v1/client/touch?redirect_url=' + redirectUrl); this.navigate( this.buildUrlWithAuth( this.#fapiClient From afd70507ce265e64061023c6cf9e97c8879d86a3 Mon Sep 17 00:00:00 2001 From: issuedat <165281975+issuedat@users.noreply.github.com> Date: Fri, 18 Oct 2024 16:59:30 +0200 Subject: [PATCH 04/15] chore: use absolute URLs in redirect param --- packages/clerk-js/src/core/clerk.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 7b6b5d43036..d5f197d85a8 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -842,6 +842,7 @@ export class Clerk implements ClerkInterface { this.client.cookieExpiresAt && this.client.cookieExpiresAt.getTime() - Date.now() <= 8 * 24 * 60 * 60 * 1000 // 8 days ) { + const absoluteRedirectUrl = new URL(redirectUrl, window.location.href); this.navigate( this.buildUrlWithAuth( this.#fapiClient @@ -849,7 +850,7 @@ export class Clerk implements ClerkInterface { method: 'GET', path: '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/client/touch', pathPrefix: 'v1', - search: { redirect_url: redirectUrl }, + search: { redirect_url: absoluteRedirectUrl.toString() }, }) .toString(), ), From 6ade57a71fa546b972242e49d2eeebac64097dde Mon Sep 17 00:00:00 2001 From: issuedat <165281975+issuedat@users.noreply.github.com> Date: Mon, 21 Oct 2024 10:51:29 +0200 Subject: [PATCH 05/15] chore: changeset and remove console.log --- .changeset/eighty-guests-change.md | 8 ++++++++ packages/clerk-js/src/core/clerk.ts | 1 + 2 files changed, 9 insertions(+) create mode 100644 .changeset/eighty-guests-change.md diff --git a/.changeset/eighty-guests-change.md b/.changeset/eighty-guests-change.md new file mode 100644 index 00000000000..3083597c4b5 --- /dev/null +++ b/.changeset/eighty-guests-change.md @@ -0,0 +1,8 @@ +--- +"@clerk/clerk-js": minor +"@clerk/types": minor +"@clerk/elements": patch +"@clerk/clerk-react": patch +--- + +Introduce redirectUrl property on setActive as a replacement for beforeEmit diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index d5f197d85a8..14650c0588d 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -843,6 +843,7 @@ export class Clerk implements ClerkInterface { this.client.cookieExpiresAt.getTime() - Date.now() <= 8 * 24 * 60 * 60 * 1000 // 8 days ) { const absoluteRedirectUrl = new URL(redirectUrl, window.location.href); + this.navigate( this.buildUrlWithAuth( this.#fapiClient From d4a05726078d0c4b5e7086d16ef71b4e9100f79e Mon Sep 17 00:00:00 2001 From: issuedat <165281975+issuedat@users.noreply.github.com> Date: Mon, 21 Oct 2024 17:30:59 +0200 Subject: [PATCH 06/15] chore: update unit tests --- .../clerk-js/src/core/__tests__/clerk.test.ts | 64 +++++++++++++------ 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index bbea30c6217..6b8b7f2e98f 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -496,6 +496,7 @@ describe('Clerk singleton', () => { expect(sut.setActive).toHaveBeenCalledWith({ session: null, beforeEmit: expect.any(Function), + redirectUrl: '/', }); }); }); @@ -518,7 +519,11 @@ describe('Clerk singleton', () => { expect(mockClientDestroy).not.toHaveBeenCalled(); expect(mockClientRemoveSessions).toHaveBeenCalled(); expect(mockSession1.remove).not.toHaveBeenCalled(); - expect(sut.setActive).toHaveBeenCalledWith({ session: null, beforeEmit: expect.any(Function) }); + expect(sut.setActive).toHaveBeenCalledWith({ + session: null, + beforeEmit: expect.any(Function), + redirectUrl: '/', + }); }); }); @@ -561,7 +566,11 @@ describe('Clerk singleton', () => { await waitFor(() => { expect(mockSession1.remove).toHaveBeenCalled(); expect(mockClientDestroy).not.toHaveBeenCalled(); - expect(sut.setActive).toHaveBeenCalledWith({ session: null, beforeEmit: expect.any(Function) }); + expect(sut.setActive).toHaveBeenCalledWith({ + session: null, + beforeEmit: expect.any(Function), + redirectUrl: '/', + }); }); }); @@ -582,7 +591,11 @@ describe('Clerk singleton', () => { await waitFor(() => { expect(mockSession1.remove).toHaveBeenCalled(); expect(mockClientDestroy).not.toHaveBeenCalled(); - expect(sut.setActive).toHaveBeenCalledWith({ session: null, beforeEmit: expect.any(Function) }); + expect(sut.setActive).toHaveBeenCalledWith({ + session: null, + beforeEmit: expect.any(Function), + redirectUrl: '/after-sign-out', + }); expect(sut.navigate).toHaveBeenCalledWith('/after-sign-out'); }); }); @@ -1096,6 +1109,16 @@ describe('Clerk singleton', () => { }); it('redirects the user to the signInForceRedirectUrl if one was provided', async () => { + const sessionId = 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe'; + const mockSession = { + id: sessionId, + remove: jest.fn(), + status: 'active', + user: {}, + touch: jest.fn(() => Promise.resolve()), + getToken: jest.fn(), + lastActiveToken: { getRawString: () => 'mocked-token' }, + }; mockEnvironmentFetch.mockReturnValue( Promise.resolve({ authConfig: {}, @@ -1110,6 +1133,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ + sessions: [mockSession], activeSessions: [], signIn: new SignIn(null), signUp: new SignUp({ @@ -1124,7 +1148,7 @@ describe('Clerk singleton', () => { long_message: "You're already signed in", message: "You're already signed in", meta: { - session_id: 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe', + session_id: sessionId, }, }, }, @@ -1133,15 +1157,9 @@ describe('Clerk singleton', () => { }), ); - const mockSetActive = jest.fn(async (setActiveOpts: any) => { - await setActiveOpts.beforeEmit(); - }); - const sut = new Clerk(productionPublishableKey); await sut.load(mockedLoadOptions); - sut.setActive = mockSetActive as any; - - sut.handleRedirectCallback({ + await sut.handleRedirectCallback({ signInForceRedirectUrl: '/custom-sign-in', }); @@ -1151,6 +1169,16 @@ describe('Clerk singleton', () => { }); it('gives priority to signInForceRedirectUrl if signInForceRedirectUrl and signInFallbackRedirectUrl were provided ', async () => { + const sessionId = 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe'; + const mockSession = { + id: sessionId, + remove: jest.fn(), + status: 'active', + user: {}, + touch: jest.fn(() => Promise.resolve()), + getToken: jest.fn(), + lastActiveToken: { getRawString: () => 'mocked-token' }, + }; mockEnvironmentFetch.mockReturnValue( Promise.resolve({ authConfig: {}, @@ -1165,6 +1193,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ + sessions: [mockSession], activeSessions: [], signIn: new SignIn(null), signUp: new SignUp({ @@ -1179,7 +1208,7 @@ describe('Clerk singleton', () => { long_message: "You're already signed in", message: "You're already signed in", meta: { - session_id: 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe', + session_id: sessionId, }, }, }, @@ -1188,15 +1217,10 @@ describe('Clerk singleton', () => { }), ); - const mockSetActive = jest.fn(async (setActiveOpts: any) => { - await setActiveOpts.beforeEmit(); - }); - const sut = new Clerk(productionPublishableKey); await sut.load(mockedLoadOptions); - sut.setActive = mockSetActive as any; - sut.handleRedirectCallback({ + await sut.handleRedirectCallback({ signInForceRedirectUrl: '/custom-sign-in', signInFallbackRedirectUrl: '/redirect-to', } as any); @@ -1654,7 +1678,7 @@ describe('Clerk singleton', () => { await waitFor(() => { expect(mockSetActive).toHaveBeenCalledWith({ session: createdSessionId, - beforeEmit: expect.any(Function), + redirectUrl: redirectUrlComplete, }); }); }); @@ -1714,7 +1738,7 @@ describe('Clerk singleton', () => { await waitFor(() => { expect(mockSetActive).toHaveBeenCalledWith({ session: createdSessionId, - beforeEmit: expect.any(Function), + redirectUrl: redirectUrlComplete, }); }); }); From 43ce325cba8433b6da4582e4417dce1a1f91735a Mon Sep 17 00:00:00 2001 From: issuedat <165281975+issuedat@users.noreply.github.com> Date: Mon, 21 Oct 2024 18:06:04 +0200 Subject: [PATCH 07/15] chore: add test cases for new redirect logic --- .../clerk-js/src/core/__tests__/clerk.test.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index 6b8b7f2e98f..0becc475d4c 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -366,6 +366,56 @@ describe('Clerk singleton', () => { }); }); + it('redirects the user to the /v1/client/touch endpoint if the cookie_expires_at is less than 8 days away', async () => { + mockSession.touch.mockReturnValue(Promise.resolve()); + mockClientFetch.mockReturnValue( + Promise.resolve({ + activeSessions: [mockSession], + cookieExpiresAt: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now + }), + ); + + const sut = new Clerk(productionPublishableKey); + sut.navigate = jest.fn(); + await sut.load(); + await sut.setActive({ session: mockSession as any as ActiveSessionResource, redirectUrl: '/redirect-url-path' }); + const redirectUrl = new URL((sut.navigate as jest.Mock).mock.calls[0]); + expect(redirectUrl.pathname).toEqual('/v1/client/touch'); + expect(redirectUrl.searchParams.get('redirect_url')).toEqual(`${mockWindowLocation.href}/redirect-url-path`); + }); + + it('does not redirect the user to the /v1/client/touch endpoint if the cookie_expires_at is more than 8 days away', async () => { + mockSession.touch.mockReturnValue(Promise.resolve()); + mockClientFetch.mockReturnValue( + Promise.resolve({ + activeSessions: [mockSession], + cookieExpiresAt: new Date(Date.now() + 10 * 24 * 60 * 60 * 1000), // 10 days from now + }), + ); + + const sut = new Clerk(productionPublishableKey); + sut.navigate = jest.fn(); + await sut.load(); + await sut.setActive({ session: mockSession as any as ActiveSessionResource, redirectUrl: '/redirect-url-path' }); + expect(sut.navigate).toHaveBeenCalledWith('/redirect-url-path'); + }); + + it('does not redirect the user to the /v1/client/touch endpoint if the cookie_expires_at is not set', async () => { + mockSession.touch.mockReturnValue(Promise.resolve()); + mockClientFetch.mockReturnValue( + Promise.resolve({ + activeSessions: [mockSession], + cookieExpiresAt: null, + }), + ); + + const sut = new Clerk(productionPublishableKey); + sut.navigate = jest.fn(); + await sut.load(); + await sut.setActive({ session: mockSession as any as ActiveSessionResource, redirectUrl: '/redirect-url-path' }); + expect(sut.navigate).toHaveBeenCalledWith('/redirect-url-path'); + }); + mockNativeRuntime(() => { it('calls session.touch in a non-standard browser', async () => { mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] })); From 356b4fbcea7b2c54bc62686011a1ff8f66cdeced Mon Sep 17 00:00:00 2001 From: issuedat <165281975+issuedat@users.noreply.github.com> Date: Tue, 22 Oct 2024 09:05:20 +0200 Subject: [PATCH 08/15] chore: fix package-lock --- package-lock.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index cb1aac77c73..eeaa0d41476 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13394,7 +13394,9 @@ } }, "node_modules/@tanstack/react-cross-context": { - "version": "1.60.0", + "version": "1.74.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-cross-context/-/react-cross-context-1.74.5.tgz", + "integrity": "sha512-a4BoKe1umpt4mmol2fUc7S11ilYIrn60ysoLot0NE+BeRsML83MvgPsLTAx7h1fTp0gmLjtpMjjubIJ8GlDIQg==", "dev": true, "license": "MIT", "funding": { @@ -13407,7 +13409,9 @@ } }, "node_modules/@tanstack/react-store": { - "version": "0.5.5", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.5.6.tgz", + "integrity": "sha512-SitIpS5jTj28DajjLpWbIX+YetmJL+6PRY0DKKiCGBKfYIqj3ryODQYF3jB3SNoR9ifUA/jFkqbJdBKFtWd+AQ==", "dev": true, "license": "MIT", "dependencies": { From 9ff6ce8b1c36280e1f6a808bd4d30874c0ff3723 Mon Sep 17 00:00:00 2001 From: issuedat <165281975+issuedat@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:03:37 +0200 Subject: [PATCH 09/15] chore: add unload tracking --- packages/clerk-js/src/core/clerk.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 14650c0588d..3db837f5875 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -838,13 +838,16 @@ export class Clerk implements ClerkInterface { } if (redirectUrl) { + beforeUnloadTracker?.startTracking(); + this.#setTransitiveState(); + if ( this.client.cookieExpiresAt && this.client.cookieExpiresAt.getTime() - Date.now() <= 8 * 24 * 60 * 60 * 1000 // 8 days ) { const absoluteRedirectUrl = new URL(redirectUrl, window.location.href); - this.navigate( + await this.navigate( this.buildUrlWithAuth( this.#fapiClient .buildUrl({ @@ -857,8 +860,9 @@ export class Clerk implements ClerkInterface { ), ); } else { - this.navigate(redirectUrl); + await this.navigate(redirectUrl); } + beforeUnloadTracker?.stopTracking(); } //3. Check if hard reloading (onbeforeunload). If not, set the user/session and emit From 60ba16ccb927b110d65887e71554ab4d2643ba7f Mon Sep 17 00:00:00 2001 From: issuedat <165281975+issuedat@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:28:59 +0200 Subject: [PATCH 10/15] lint --- packages/clerk-js/src/core/clerk.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 3db837f5875..77e830133ae 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -862,6 +862,7 @@ export class Clerk implements ClerkInterface { } else { await this.navigate(redirectUrl); } + beforeUnloadTracker?.stopTracking(); } From 641a0045e399a777e39071a5060cac76fb7ac868 Mon Sep 17 00:00:00 2001 From: issuedat <165281975+issuedat@users.noreply.github.com> Date: Thu, 24 Oct 2024 09:24:46 +0200 Subject: [PATCH 11/15] chore: add deprecation warning --- packages/clerk-js/src/core/clerk.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 77e830133ae..5847551beab 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1,6 +1,7 @@ import { addClerkPrefix, ClerkRuntimeError, + deprecated, handleValueOrFn, inBrowser as inClientSide, is4xxError, @@ -833,6 +834,7 @@ export class Clerk implements ClerkInterface { if (beforeEmit) { beforeUnloadTracker?.startTracking(); this.#setTransitiveState(); + deprecated('beforeEmit', 'Use the `redirectUrl` property instead.'); await beforeEmit(newSession); beforeUnloadTracker?.stopTracking(); } From 6e894de62f5d2ab9181a375187573639ef812a72 Mon Sep 17 00:00:00 2001 From: issuedat <165281975+issuedat@users.noreply.github.com> Date: Mon, 28 Oct 2024 16:27:13 +0100 Subject: [PATCH 12/15] chore: abstract away logic for redirect decision to client method --- packages/clerk-js/src/core/__tests__/clerk.test.ts | 5 +++++ packages/clerk-js/src/core/clerk.ts | 5 +---- packages/clerk-js/src/core/resources/Client.ts | 5 +++++ packages/types/src/client.ts | 1 + 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index 0becc475d4c..859825cba1b 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -372,6 +372,7 @@ describe('Clerk singleton', () => { Promise.resolve({ activeSessions: [mockSession], cookieExpiresAt: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now + isEligibleForTouch: () => true, }), ); @@ -390,6 +391,7 @@ describe('Clerk singleton', () => { Promise.resolve({ activeSessions: [mockSession], cookieExpiresAt: new Date(Date.now() + 10 * 24 * 60 * 60 * 1000), // 10 days from now + isEligibleForTouch: () => false, }), ); @@ -406,6 +408,7 @@ describe('Clerk singleton', () => { Promise.resolve({ activeSessions: [mockSession], cookieExpiresAt: null, + isEligibleForTouch: () => false, }), ); @@ -1204,6 +1207,7 @@ describe('Clerk singleton', () => { }, }, } as any as SignUpJSON), + isEligibleForTouch: () => false, }), ); @@ -1264,6 +1268,7 @@ describe('Clerk singleton', () => { }, }, } as any as SignUpJSON), + isEligibleForTouch: () => false, }), ); diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 5847551beab..b9d4001b33b 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -843,10 +843,7 @@ export class Clerk implements ClerkInterface { beforeUnloadTracker?.startTracking(); this.#setTransitiveState(); - if ( - this.client.cookieExpiresAt && - this.client.cookieExpiresAt.getTime() - Date.now() <= 8 * 24 * 60 * 60 * 1000 // 8 days - ) { + if (this.client.isEligibleForTouch()) { const absoluteRedirectUrl = new URL(redirectUrl, window.location.href); await this.navigate( diff --git a/packages/clerk-js/src/core/resources/Client.ts b/packages/clerk-js/src/core/resources/Client.ts index 0336a3308a1..8368b964ab8 100644 --- a/packages/clerk-js/src/core/resources/Client.ts +++ b/packages/clerk-js/src/core/resources/Client.ts @@ -78,6 +78,11 @@ export class Client extends BaseResource implements ClientResource { return this.sessions.forEach(s => s.clearCache()); } + // isEligibleForTouch returns true if the client cookie is due to expire in 8 days or less + isEligibleForTouch(): boolean { + return !!this.cookieExpiresAt && this.cookieExpiresAt.getTime() - Date.now() <= 8 * 24 * 60 * 60 * 1000; // 8 days + } + fromJSON(data: ClientJSON | null): this { if (data) { this.id = data.id; diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index 471035bf3e4..62be5d5732b 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -13,6 +13,7 @@ export interface ClientResource extends ClerkResource { destroy: () => Promise; removeSessions: () => Promise; clearCache: () => void; + isEligibleForTouch: () => boolean; lastActiveSessionId: string | null; cookieExpiresAt: Date | null; createdAt: Date | null; From f2d80001e3d8f60569026f4842c2221a6911a2c4 Mon Sep 17 00:00:00 2001 From: issuedat <165281975+issuedat@users.noreply.github.com> Date: Mon, 28 Oct 2024 17:09:45 +0100 Subject: [PATCH 13/15] chore: add buildTouchUrl method to client to construct touch URL --- packages/clerk-js/src/core/__tests__/clerk.test.ts | 6 ++++++ packages/clerk-js/src/core/clerk.ts | 13 +------------ packages/clerk-js/src/core/resources/Client.ts | 11 +++++++++++ packages/types/src/client.ts | 1 + 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index 859825cba1b..25d5f8978a6 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -373,6 +373,8 @@ describe('Clerk singleton', () => { activeSessions: [mockSession], cookieExpiresAt: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now isEligibleForTouch: () => true, + buildTouchUrl: () => + `https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`, }), ); @@ -392,6 +394,8 @@ describe('Clerk singleton', () => { activeSessions: [mockSession], cookieExpiresAt: new Date(Date.now() + 10 * 24 * 60 * 60 * 1000), // 10 days from now isEligibleForTouch: () => false, + buildTouchUrl: () => + `https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`, }), ); @@ -409,6 +413,8 @@ describe('Clerk singleton', () => { activeSessions: [mockSession], cookieExpiresAt: null, isEligibleForTouch: () => false, + buildTouchUrl: () => + `https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`, }), ); diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index b9d4001b33b..99c9c905965 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -846,18 +846,7 @@ export class Clerk implements ClerkInterface { if (this.client.isEligibleForTouch()) { const absoluteRedirectUrl = new URL(redirectUrl, window.location.href); - await this.navigate( - this.buildUrlWithAuth( - this.#fapiClient - .buildUrl({ - method: 'GET', - path: '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/client/touch', - pathPrefix: 'v1', - search: { redirect_url: absoluteRedirectUrl.toString() }, - }) - .toString(), - ), - ); + await this.navigate(this.buildUrlWithAuth(this.client.buildTouchUrl({ redirectUrl: absoluteRedirectUrl }))); } else { await this.navigate(redirectUrl); } diff --git a/packages/clerk-js/src/core/resources/Client.ts b/packages/clerk-js/src/core/resources/Client.ts index 8368b964ab8..8e4ac8ed962 100644 --- a/packages/clerk-js/src/core/resources/Client.ts +++ b/packages/clerk-js/src/core/resources/Client.ts @@ -83,6 +83,17 @@ export class Client extends BaseResource implements ClientResource { return !!this.cookieExpiresAt && this.cookieExpiresAt.getTime() - Date.now() <= 8 * 24 * 60 * 60 * 1000; // 8 days } + buildTouchUrl({ redirectUrl }: { redirectUrl: URL }) { + return BaseResource.fapiClient + .buildUrl({ + method: 'GET', + path: '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/client/touch', + pathPrefix: 'v1', + search: { redirect_url: redirectUrl.toString() }, + }) + .toString(); + } + fromJSON(data: ClientJSON | null): this { if (data) { this.id = data.id; diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index 62be5d5732b..a3e06b8ce6a 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -14,6 +14,7 @@ export interface ClientResource extends ClerkResource { removeSessions: () => Promise; clearCache: () => void; isEligibleForTouch: () => boolean; + buildTouchUrl: (params: { redirectUrl: URL }) => string; lastActiveSessionId: string | null; cookieExpiresAt: Date | null; createdAt: Date | null; From 2af1ea2934ba67bfca79b2cc8b1669386efb32bb Mon Sep 17 00:00:00 2001 From: issuedat <165281975+issuedat@users.noreply.github.com> Date: Tue, 29 Oct 2024 11:26:07 +0100 Subject: [PATCH 14/15] Update .changeset/eighty-guests-change.md Co-authored-by: panteliselef --- .changeset/eighty-guests-change.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/eighty-guests-change.md b/.changeset/eighty-guests-change.md index 3083597c4b5..90339edec3c 100644 --- a/.changeset/eighty-guests-change.md +++ b/.changeset/eighty-guests-change.md @@ -5,4 +5,5 @@ "@clerk/clerk-react": patch --- -Introduce redirectUrl property on setActive as a replacement for beforeEmit +Introduce `redirectUrl` property on `setActive` as a replacement for `beforeEmit`. +- Deprecates `beforeEmit` property on `setActive`. From 0b873da0e082e153d166276a4139fb2a5a212403 Mon Sep 17 00:00:00 2001 From: issuedat <165281975+issuedat@users.noreply.github.com> Date: Tue, 29 Oct 2024 11:28:51 +0100 Subject: [PATCH 15/15] chore: format changeset --- .changeset/eighty-guests-change.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/eighty-guests-change.md b/.changeset/eighty-guests-change.md index 90339edec3c..51a2432c1f9 100644 --- a/.changeset/eighty-guests-change.md +++ b/.changeset/eighty-guests-change.md @@ -5,5 +5,5 @@ "@clerk/clerk-react": patch --- -Introduce `redirectUrl` property on `setActive` as a replacement for `beforeEmit`. +- Introduce `redirectUrl` property on `setActive` as a replacement for `beforeEmit`. - Deprecates `beforeEmit` property on `setActive`.