diff --git a/src/app/[locale]/complete-registration/page.tsx b/src/app/[locale]/complete-registration/page.tsx index e606231e..775b2d58 100644 --- a/src/app/[locale]/complete-registration/page.tsx +++ b/src/app/[locale]/complete-registration/page.tsx @@ -1,14 +1,14 @@ import { type ReactElement } from 'react'; import CompleteRegistration from './CompleteRegistration'; import { ReduxGateWrapper } from '../../components/ReduxGateWrapper'; +import { ProtectedPageWrapper } from '../../components/ProtectedPageWrapper'; export default function CompleteRegistrationPage(): ReactElement { return ( - {/* TODO: Revisit protected page wrappers. This page changes the status of the user which causes flickers of mismatched authentication */} - {/* */} - - {/* */} + + + ); } diff --git a/src/app/[locale]/verify-email/page.tsx b/src/app/[locale]/verify-email/page.tsx index e2a931a3..a26f0ef7 100644 --- a/src/app/[locale]/verify-email/page.tsx +++ b/src/app/[locale]/verify-email/page.tsx @@ -1,14 +1,14 @@ import { type ReactElement } from 'react'; import PostRegistration from './PostRegistration'; import { ReduxGateWrapper } from '../../components/ReduxGateWrapper'; +import { ProtectedPageWrapper } from '../../components/ProtectedPageWrapper'; export default function VerifyEmailPage(): ReactElement { return ( - {/* TODO: Revisit protected page wrappers. This page changes the status of the user which causes flickers of mismatched authentication */} - {/* */} - - {/* */} + + + ); } diff --git a/src/app/components/ProtectedPageWrapper.tsx b/src/app/components/ProtectedPageWrapper.tsx index 9b8b2c35..9ed11a31 100644 --- a/src/app/components/ProtectedPageWrapper.tsx +++ b/src/app/components/ProtectedPageWrapper.tsx @@ -5,21 +5,22 @@ // targetStatus can be used to scope groups further (e.g. (authenticated), (unverified)). import { useEffect } from 'react'; import { useSelector } from 'react-redux'; -import { useRouter } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; import { useAppDispatch } from '../hooks'; import { refreshApp, refreshAppSuccess } from '../store/profile-reducer'; import { selectUserProfileStatus } from '../store/selectors'; import { app } from '../../firebase'; import { SIGN_IN_TARGET } from '../constants/Navigation'; +import { useAuthSession } from './AuthSessionProvider'; interface ProtectedPageWrapperProps { children: React.ReactNode; /** - * The user profile status required to access this page. + * The user profile status or statuses required to access this page. * Mirrors the `targetStatus` prop from the legacy ProtectedRoute component. * Defaults to 'registered'. */ - targetStatus?: string; + targetStatus?: string | string[]; redirect?: string; } @@ -29,9 +30,15 @@ export function ProtectedPageWrapper({ redirect = SIGN_IN_TARGET, }: ProtectedPageWrapperProps): React.ReactElement | null { const userProfileStatus = useSelector(selectUserProfileStatus); + const { isAuthResolved } = useAuthSession(); const router = useRouter(); + const searchParams = useSearchParams(); const dispatch = useAppDispatch(); + const allowedStatuses = Array.isArray(targetStatus) + ? targetStatus + : [targetStatus]; + useEffect(() => { app.auth(); @@ -51,13 +58,29 @@ export function ProtectedPageWrapper({ }; }, [dispatch]); + const isAuthorized = allowedStatuses.includes(userProfileStatus); + useEffect(() => { - if (userProfileStatus !== targetStatus) { - router.replace(redirect); + // Only redirect once auth state has resolved and profile is hydrated + if (!isAuthResolved) return; + if (userProfileStatus === 'idle' || userProfileStatus === 'loading') return; + + if (!isAuthorized) { + const query = searchParams?.toString(); + const target = + query && !redirect.includes('?') ? `${redirect}?${query}` : redirect; + router.replace(target); } - }, [userProfileStatus, targetStatus, redirect, router]); + }, [ + isAuthResolved, + userProfileStatus, + isAuthorized, + redirect, + router, + searchParams, + ]); - if (userProfileStatus !== targetStatus) { + if (!isAuthResolved || !isAuthorized) { return null; }