mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
fix(flows): bounce-to-dashboard race when navigating into /flows
use-auth's `loading` flips false as soon as the session settles —
the profile fetch is fire-and-forget so the page renders quickly
with just the user object. The flows pages, though, gate access on
`isFlowsEnabled(profile.beta_features)`. During the gap between
session-ready and profile-ready they saw `{ loading: false,
profile: null }` and read it as "not opted in", redirecting a
legitimate beta user back to /dashboard.
Expose a separate `profileLoading` boolean from the auth context
that stays true until fetchProfile settles (success, missing row,
or error). Pages that branch on `profile.*` gate on it; chrome
that only needs the user object keeps using `loading` as before.
Three call sites updated: /flows, /flows/[id], /flows/[id]/runs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1a6419b815
commit
6fcf77de8c
@ -23,7 +23,7 @@ import type { FlowRow, FlowNodeRow } from "@/lib/flows/types";
|
||||
export default function FlowEditorPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams<{ id: string }>();
|
||||
const { profile, loading: authLoading } = useAuth();
|
||||
const { profile, loading: authLoading, profileLoading } = useAuth();
|
||||
|
||||
const [flow, setFlow] = useState<FlowRow | null>(null);
|
||||
const [nodes, setNodes] = useState<FlowNodeRow[]>([]);
|
||||
@ -33,7 +33,11 @@ export default function FlowEditorPage() {
|
||||
const allowed = isFlowsEnabled(profile);
|
||||
|
||||
useEffect(() => {
|
||||
if (authLoading) return;
|
||||
// Wait for BOTH session and profile — see the comment on the same
|
||||
// gate in /flows/page.tsx. Without `profileLoading`, navigating
|
||||
// here from the list shows the `{ loading: false, profile: null }`
|
||||
// window and would bounce a legitimate beta user.
|
||||
if (authLoading || profileLoading) return;
|
||||
if (!allowed) {
|
||||
router.replace("/dashboard");
|
||||
return;
|
||||
@ -68,9 +72,9 @@ export default function FlowEditorPage() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [allowed, authLoading, params.id, router]);
|
||||
}, [allowed, authLoading, profileLoading, params.id, router]);
|
||||
|
||||
if (authLoading || (allowed && loading)) {
|
||||
if (authLoading || profileLoading || (allowed && loading)) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-slate-500" />
|
||||
|
||||
@ -98,7 +98,7 @@ const STATUS_META: Record<
|
||||
export default function FlowRunsPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams<{ id: string }>();
|
||||
const { profile, loading: authLoading } = useAuth();
|
||||
const { profile, loading: authLoading, profileLoading } = useAuth();
|
||||
|
||||
const [flow, setFlow] = useState<{ id: string; name: string } | null>(null);
|
||||
const [runs, setRuns] = useState<RunRow[]>([]);
|
||||
@ -110,7 +110,8 @@ export default function FlowRunsPage() {
|
||||
const allowed = isFlowsEnabled(profile);
|
||||
|
||||
useEffect(() => {
|
||||
if (authLoading) return;
|
||||
// Wait for BOTH session and profile — see /flows/page.tsx.
|
||||
if (authLoading || profileLoading) return;
|
||||
if (!allowed) {
|
||||
router.replace("/dashboard");
|
||||
return;
|
||||
@ -147,7 +148,7 @@ export default function FlowRunsPage() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [allowed, authLoading, params.id, router]);
|
||||
}, [allowed, authLoading, profileLoading, params.id, router]);
|
||||
|
||||
function toggle(runId: string) {
|
||||
setExpanded((prev) => {
|
||||
@ -158,7 +159,7 @@ export default function FlowRunsPage() {
|
||||
});
|
||||
}
|
||||
|
||||
if (authLoading || (allowed && loading)) {
|
||||
if (authLoading || profileLoading || (allowed && loading)) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-slate-500" />
|
||||
|
||||
@ -84,7 +84,7 @@ const TEMPLATE_ICONS = {
|
||||
|
||||
export default function FlowsPage() {
|
||||
const router = useRouter();
|
||||
const { profile, loading: authLoading } = useAuth();
|
||||
const { profile, loading: authLoading, profileLoading } = useAuth();
|
||||
const [flows, setFlows] = useState<FlowRow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
@ -95,7 +95,12 @@ export default function FlowsPage() {
|
||||
const flowsAccessAllowed = isFlowsEnabled(profile);
|
||||
|
||||
useEffect(() => {
|
||||
if (authLoading) return;
|
||||
// Wait for BOTH the session and the profile row. Session loads
|
||||
// fast (cookie read) but profile crosses the network — during the
|
||||
// gap `profile` is null and `isFlowsEnabled(null)` is false, which
|
||||
// would bounce a legitimate beta user back to /dashboard on every
|
||||
// navigation here.
|
||||
if (authLoading || profileLoading) return;
|
||||
if (!flowsAccessAllowed) {
|
||||
// Bounce non-beta users — the API would 404 every call anyway.
|
||||
router.replace("/dashboard");
|
||||
@ -133,7 +138,7 @@ export default function FlowsPage() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [authLoading, flowsAccessAllowed, router]);
|
||||
}, [authLoading, profileLoading, flowsAccessAllowed, router]);
|
||||
|
||||
async function handleCreate() {
|
||||
if (!newName.trim()) return;
|
||||
@ -200,7 +205,7 @@ export default function FlowsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
if (authLoading || (flowsAccessAllowed && loading)) {
|
||||
if (authLoading || profileLoading || (flowsAccessAllowed && loading)) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-slate-500" />
|
||||
|
||||
@ -29,7 +29,21 @@ interface Profile {
|
||||
interface AuthContextValue {
|
||||
user: User | null;
|
||||
profile: Profile | null;
|
||||
/**
|
||||
* Session-level loading. Flips to false as soon as we know whether
|
||||
* a user is signed in, *without* waiting for the profile row. Use
|
||||
* this for chrome (sidebar / header) that can render with just the
|
||||
* user object.
|
||||
*/
|
||||
loading: boolean;
|
||||
/**
|
||||
* Profile-row loading. Stays true until `fetchProfile` settles
|
||||
* (success, missing row, or error). Code that branches on
|
||||
* `profile.beta_features` MUST gate on this — otherwise it sees the
|
||||
* `{ loading: false, profile: null }` window during initial load
|
||||
* and may take the "not opted in" branch incorrectly.
|
||||
*/
|
||||
profileLoading: boolean;
|
||||
signOut: () => Promise<void>;
|
||||
/** Re-fetch the current user's profile row — call after a save from
|
||||
* the settings form so header/sidebar reflect the change without a
|
||||
@ -48,12 +62,18 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [profile, setProfile] = useState<Profile | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
// Tracked separately from `loading`. The session settles fast (one
|
||||
// local cookie read); the profile fetch crosses the network and
|
||||
// settles later. Callers that gate on `profile.*` need to know which
|
||||
// window they're in — see the type doc above.
|
||||
const [profileLoading, setProfileLoading] = useState(true);
|
||||
|
||||
// Shared across init, auth-state-change listener, and the exposed
|
||||
// refreshProfile() callback. Reads the current session's user id and
|
||||
// pulls the matching profile row.
|
||||
const fetchProfile = useCallback(async (userId: string) => {
|
||||
const supabase = createClient();
|
||||
setProfileLoading(true);
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from("profiles")
|
||||
@ -83,6 +103,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[AuthProvider] fetchProfile threw:", err);
|
||||
} finally {
|
||||
setProfileLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@ -94,6 +116,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
if (mounted) {
|
||||
console.warn("[AuthProvider] getSession() timed out after 3s");
|
||||
setLoading(false);
|
||||
setProfileLoading(false);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
@ -111,9 +134,16 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
setUser(currentUser);
|
||||
|
||||
if (currentUser) {
|
||||
// Don't block loading on profile fetch — let the UI render
|
||||
// with the user info we already have, profile enriches async.
|
||||
// Don't block session loading on profile fetch — chrome
|
||||
// (header, sidebar) can render from the user object alone,
|
||||
// profile enriches async. Callers that need to branch on
|
||||
// profile data gate on `profileLoading` instead.
|
||||
fetchProfile(currentUser.id);
|
||||
} else {
|
||||
// No user → no profile to load. Flip profileLoading off so
|
||||
// pages that gate on it don't wait forever on the logged-out
|
||||
// path (the route guard or redirect should fire instead).
|
||||
setProfileLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[AuthProvider] init threw:", err);
|
||||
@ -136,6 +166,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
fetchProfile(currentUser.id);
|
||||
} else {
|
||||
setProfile(null);
|
||||
setProfileLoading(false);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
@ -163,7 +194,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{ user, profile, loading, signOut, refreshProfile }}
|
||||
value={{ user, profile, loading, profileLoading, signOut, refreshProfile }}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
@ -183,6 +214,7 @@ export function useAuth(): AuthContextValue {
|
||||
user: null,
|
||||
profile: null,
|
||||
loading: false,
|
||||
profileLoading: false,
|
||||
signOut: async () => {
|
||||
window.location.href = "/login";
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user