From a67abd3fbc8337f0c0525c1939a85107c7a6fd6a Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Wed, 10 Dec 2025 12:13:41 +0100 Subject: [PATCH] feat(client): redirect from /settings + /update-email after sign out (#64405) --- client/src/components/signout-modal/index.tsx | 29 ++++++++++++++--- .../signout-modal/signout-modal.test.ts | 31 +++++++++++++++++++ 2 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 client/src/components/signout-modal/signout-modal.test.ts diff --git a/client/src/components/signout-modal/index.tsx b/client/src/components/signout-modal/index.tsx index 46073553260..72fa7e12f8e 100644 --- a/client/src/components/signout-modal/index.tsx +++ b/client/src/components/signout-modal/index.tsx @@ -5,7 +5,7 @@ import { connect } from 'react-redux'; import { useTranslation } from 'react-i18next'; import { Button, Modal, Spacer } from '@freecodecamp/ui'; -import { hardGoTo as navigate, closeSignoutModal } from '../../redux/actions'; +import { closeSignoutModal } from '../../redux/actions'; import { isSignoutModalOpenSelector } from '../../redux/selectors'; import { apiLocation } from '../../../config/env.json'; import callGA from '../../analytics/call-ga'; @@ -20,20 +20,31 @@ const mapStateToProps = createSelector( const mapDispatchToProps = (dispatch: Dispatch) => bindActionCreators( { - navigate, closeSignoutModal }, dispatch ); type SignoutModalProps = { - navigate: (path: string) => void; closeSignoutModal: () => void; show: boolean; }; +export const pathAfterSignout = (currentPath: string): string => { + // These pages try to sign in the user automatically if they are signed out, + // so, to respect the user's intention to sign out, we redirect them to /learn + // instead. + const redirectedPaths = ['/settings', '/update-email']; + const allPaths = [ + ...redirectedPaths, + ...redirectedPaths.map(path => `${path}/`) + ]; + + return allPaths.some(path => currentPath === path) ? '/learn' : currentPath; +}; + function SignoutModal(props: SignoutModalProps): JSX.Element { - const { show, closeSignoutModal, navigate } = props; + const { show, closeSignoutModal } = props; const { t } = useTranslation(); const handleModalHide = () => { @@ -43,7 +54,15 @@ function SignoutModal(props: SignoutModalProps): JSX.Element { const handleSignout = () => { closeSignoutModal(); callGA({ event: 'sign_out', user_id: undefined }); - navigate(`${apiLocation}/signout`); + const redirect = () => { + window.location.pathname = pathAfterSignout(window.location.pathname); + }; + void fetch(`${apiLocation}/signout`, { + method: 'GET', + credentials: 'include' + }) + .then(redirect) + .catch(redirect); }; return ( diff --git a/client/src/components/signout-modal/signout-modal.test.ts b/client/src/components/signout-modal/signout-modal.test.ts new file mode 100644 index 00000000000..50a2511e039 --- /dev/null +++ b/client/src/components/signout-modal/signout-modal.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from 'vitest'; + +import { pathAfterSignout } from '.'; + +describe('pathAfterSignout', () => { + it('should default to the supplied path', () => { + const unexceptionalPath = 'a/normal/path/'; + expect(pathAfterSignout(unexceptionalPath)).toBe(unexceptionalPath); + }); + + it('should redirect paths that automatically sign in back to /learn', () => { + const pathsThatAttemptToSignIn = ['/settings', '/update-email']; + const newPaths = pathsThatAttemptToSignIn.map(pathAfterSignout); + + expect(newPaths).toEqual(['/learn', '/learn']); + }); + + it('should redirect paths with trailing slashes', () => { + const pathsThatAttemptToSignIn = ['/settings/', '/update-email/']; + const newPaths = pathsThatAttemptToSignIn.map(pathAfterSignout); + + expect(newPaths).toEqual(['/learn', '/learn']); + }); + + it('should only redirect exact matches', () => { + const similarPaths = ['/settingss', '/update-emails/', '/settings/2']; + const newPaths = similarPaths.map(pathAfterSignout); + + expect(newPaths).toEqual(similarPaths); + }); +});