Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions frontend/src/components/VerificationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/routes/auth.$provider.callback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: "/" });
}
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/routes/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/routes/signup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/routes/team.invite.$inviteId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -168,6 +171,7 @@ function TeamInviteAcceptance() {
return (
<>
<TopNav />
<VerificationModal />
<FullPageMain>
<div className="flex items-center justify-center min-h-[60vh]">
<Card className="w-full max-w-md">
Expand Down
14 changes: 13 additions & 1 deletion frontend/src/routes/verify.$code.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment on lines 25 to 40
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid async side effects inside setTimeout here.

refetchUser()/redirect failures inside the timer bypass the outer try/catch, so failures become unhandled and the flow can silently stall.

Proposed fix
-        // Do both refetch and navigation after a delay
-        setTimeout(async () => {
-          await refetchUser();
-
-          // Check for a pending redirect (e.g. team invite page)
-          const pendingRedirect = sessionStorage.getItem("post_auth_redirect");
-          if (pendingRedirect) {
-            sessionStorage.removeItem("post_auth_redirect");
-            navigate({ to: pendingRedirect });
-          } else {
-            navigate({ to: "/" });
-          }
-        }, 2000);
+        await new Promise((resolve) => setTimeout(resolve, 2000));
+        await refetchUser();
+
+        const pendingRedirect = sessionStorage.getItem("post_auth_redirect");
+        if (pendingRedirect) {
+          sessionStorage.removeItem("post_auth_redirect");
+          navigate({ to: pendingRedirect });
+        } else {
+          navigate({ to: "/" });
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/routes/verify`.$code.tsx around lines 25 - 36, The setTimeout
callback contains async work (await refetchUser() and navigate) which escapes
the surrounding try/catch and can produce unhandled rejections; extract the
async flow into a named async helper (e.g., handlePostAuthRedirect or similar)
that performs await refetchUser(), reads sessionStorage ("post_auth_redirect"),
removes it, and calls navigate(...) inside its own try/catch, then invoke it
from setTimeout using a non-async arrow (e.g., setTimeout(() => void
handlePostAuthRedirect(), 2000)) or call handlePostAuthRedirect().catch(err =>
{/* log/handle error */}) so all errors are caught and logged.

} catch (err) {
if (err instanceof Error) {
Expand Down
Loading