feat: remember original URL and redirect after login (#18308)

## Summary

- Implement a return-to-path mechanism that preserves the user's
intended destination across authentication flows (login, magic link,
cross-domain redirects)
- Uses layered persistence: Jotai atom (in-memory), sessionStorage with
TTL (tab-switch resilience), URL query parameter (cross-domain
propagation)
- Includes path validation to prevent open redirects, automatic cleanup
after successful login, and comprehensive test coverage
- Replaces the unused `previousUrlState` with a robust
`returnToPathState` system

## Test plan

- [ ] Visit a deep link (e.g. `/objects/tasks`) while logged out —
should redirect to login, then back to `/objects/tasks` after logging in
- [ ] Visit an OAuth authorize link while logged out — should redirect
to login, then to the authorize page
- [ ] Test magic link flow: click sign-in link that opens new tab —
should still redirect to original destination
- [ ] Test cross-domain: visit `app.twenty.com/objects/tasks` — should
preserve path through workspace domain redirect
- [ ] Verify auth/onboarding paths are excluded from being saved as
return paths
- [ ] Verify return-to-path is cleared after successful navigation
- [ ] All 215 existing `usePageChangeEffectNavigateLocation` tests pass


Made with [Cursor](https://cursor.com)
This commit is contained in:
Félix Malfait
2026-03-02 19:00:48 +01:00
committed by GitHub
parent 20a2c3836e
commit 6351c6c1c6
15 changed files with 351 additions and 45 deletions
@@ -7,7 +7,8 @@ import { currentUserState } from '@/auth/states/currentUserState';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { previousUrlState } from '@/auth/states/previousUrlState';
import { returnToPathState } from '@/auth/states/returnToPathState';
import { isValidReturnToPath } from '@/auth/utils/isValidReturnToPath';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { appVersionState } from '@/client-config/states/appVersionState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
@@ -36,7 +37,7 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
const setCurrentUser = useSetAtomState(currentUserState);
const setCurrentUserWorkspace = useSetAtomState(currentUserWorkspaceState);
const setPreviousUrl = useSetAtomState(previousUrlState);
const setReturnToPath = useSetAtomState(returnToPathState);
const location = useLocation();
const { enqueueErrorSnackBar } = useSnackBar();
@@ -76,7 +77,11 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
!isMatchingLocation(location, AppPath.Invite) &&
!isMatchingLocation(location, AppPath.ResetPassword)
) {
setPreviousUrl(`${location.pathname}${location.search}`);
const path = `${location.pathname}${location.search}${location.hash}`;
if (isValidReturnToPath(path)) {
setReturnToPath(path);
}
navigate(AppPath.SignInUp);
}
},
@@ -109,7 +114,7 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
setCurrentUser,
setCurrentWorkspaceMember,
setCurrentWorkspace,
setPreviousUrl,
setReturnToPath,
enqueueErrorSnackBar,
]);