diff --git a/frontend/src/components/VerificationModal.tsx b/frontend/src/components/VerificationModal.tsx index 247f8c4a..27bfa353 100644 --- a/frontend/src/components/VerificationModal.tsx +++ b/frontend/src/components/VerificationModal.tsx @@ -7,6 +7,7 @@ import { DialogTitle } from "@/components/ui/dialog"; import { useOpenSecret } from "@opensecret/react"; +import { useNavigate } from "@tanstack/react-router"; import { useState, useEffect } from "react"; import { Loader2, CheckCircle, LogOut } from "lucide-react"; import { Input } from "./ui/input"; @@ -15,6 +16,7 @@ import { AlertDestructive } from "./AlertDestructive"; export function VerificationModal() { const os = useOpenSecret(); + const navigate = useNavigate(); const [isOpen, setIsOpen] = useState(() => { if (!os.auth.user) return false; // Skip email verification for guest users and in local development @@ -78,6 +80,13 @@ export function VerificationModal() { setIsVerifying(true); await os.verifyEmail(verificationCode); await os.refetchUser(); + + // Check for a pending redirect (e.g. team invite page) after email verification + const pendingRedirect = sessionStorage.getItem("post_auth_redirect"); + sessionStorage.removeItem("post_auth_redirect"); + if (pendingRedirect && pendingRedirect.startsWith("/") && !pendingRedirect.startsWith("//")) { + navigate({ to: pendingRedirect }); + } } catch (err) { if (err instanceof Error) { setError(err.message); diff --git a/frontend/src/routes/auth.$provider.callback.tsx b/frontend/src/routes/auth.$provider.callback.tsx index 4687e8e1..f1332fa8 100644 --- a/frontend/src/routes/auth.$provider.callback.tsx +++ b/frontend/src/routes/auth.$provider.callback.tsx @@ -62,12 +62,21 @@ function OAuthCallback() { const selectedPlan = sessionStorage.getItem("selected_plan"); sessionStorage.removeItem("selected_plan"); + const postAuthRedirect = sessionStorage.getItem("post_auth_redirect"); + sessionStorage.removeItem("post_auth_redirect"); + const safePostAuthRedirect = + postAuthRedirect && postAuthRedirect.startsWith("/") && !postAuthRedirect.startsWith("//") + ? postAuthRedirect + : null; + setTimeout(() => { if (selectedPlan) { navigate({ to: "/pricing", search: { selected_plan: selectedPlan } }); + } else if (safePostAuthRedirect) { + navigate({ to: safePostAuthRedirect }); } else { navigate({ to: "/" }); } diff --git a/frontend/src/routes/login.tsx b/frontend/src/routes/login.tsx index 47822ae7..c25f4cfd 100644 --- a/frontend/src/routes/login.tsx +++ b/frontend/src/routes/login.tsx @@ -139,6 +139,9 @@ function LoginPage() { if (code) { sessionStorage.setItem("redeem_code", code); } + if (next && next.startsWith("/") && !next.startsWith("//")) { + sessionStorage.setItem("post_auth_redirect", next); + } window.location.href = auth_url; } } catch (error) { @@ -181,6 +184,9 @@ function LoginPage() { if (code) { sessionStorage.setItem("redeem_code", code); } + if (next && next.startsWith("/") && !next.startsWith("//")) { + sessionStorage.setItem("post_auth_redirect", next); + } window.location.href = auth_url; } } catch (error) { diff --git a/frontend/src/routes/signup.tsx b/frontend/src/routes/signup.tsx index 7fc581cb..fe0f3087 100644 --- a/frontend/src/routes/signup.tsx +++ b/frontend/src/routes/signup.tsx @@ -150,6 +150,9 @@ function SignupPage() { if (code) { sessionStorage.setItem("redeem_code", code); } + if (next && next.startsWith("/") && !next.startsWith("//")) { + sessionStorage.setItem("post_auth_redirect", next); + } window.location.href = auth_url; } } catch (error) { @@ -192,6 +195,9 @@ function SignupPage() { if (code) { sessionStorage.setItem("redeem_code", code); } + if (next && next.startsWith("/") && !next.startsWith("//")) { + sessionStorage.setItem("post_auth_redirect", next); + } window.location.href = auth_url; } } catch (error) { diff --git a/frontend/src/routes/team.invite.$inviteId.tsx b/frontend/src/routes/team.invite.$inviteId.tsx index 9b7e2924..3d552277 100644 --- a/frontend/src/routes/team.invite.$inviteId.tsx +++ b/frontend/src/routes/team.invite.$inviteId.tsx @@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Loader2, UserPlus, AlertCircle, CheckCircle, XCircle } from "lucide-react"; import type { CheckInviteResponse } from "@/types/team"; +import { VerificationModal } from "@/components/VerificationModal"; export const Route = createFileRoute("/team/invite/$inviteId")({ component: TeamInviteAcceptance @@ -44,6 +45,8 @@ function TeamInviteAcceptance() { // Redirect to signup if not authenticated useEffect(() => { if (!isLoggedIn && !checkingInvite) { + // Store the invite URL so it survives OAuth redirects and email verification + sessionStorage.setItem("post_auth_redirect", `/team/invite/${inviteId}`); navigate({ to: "/signup", search: { @@ -168,6 +171,7 @@ function TeamInviteAcceptance() { return ( <> +
diff --git a/frontend/src/routes/verify.$code.tsx b/frontend/src/routes/verify.$code.tsx index 4fa80663..ab681a06 100644 --- a/frontend/src/routes/verify.$code.tsx +++ b/frontend/src/routes/verify.$code.tsx @@ -24,7 +24,19 @@ function VerifyEmail() { // Do both refetch and navigation after a delay setTimeout(async () => { await refetchUser(); - navigate({ to: "/" }); + + // Check for a pending redirect (e.g. team invite page) + const pendingRedirect = sessionStorage.getItem("post_auth_redirect"); + sessionStorage.removeItem("post_auth_redirect"); + if ( + pendingRedirect && + pendingRedirect.startsWith("/") && + !pendingRedirect.startsWith("//") + ) { + navigate({ to: pendingRedirect }); + } else { + navigate({ to: "/" }); + } }, 2000); } catch (err) { if (err instanceof Error) {