Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
58 changes: 56 additions & 2 deletions mcpjam-inspector/client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useConvexAuth } from "convex/react";
import { useConvexAuth, useQuery } from "convex/react";
import {
useCallback,
useEffect,
Expand Down Expand Up @@ -81,6 +81,7 @@ import {
} from "./lib/theme-utils";
import CompletingSignInLoading from "./components/CompletingSignInLoading";
import LoadingScreen from "./components/LoadingScreen";
import { OccupationGate } from "./components/signup/OccupationGate";
import { Header } from "./components/Header";
import { ThemePreset } from "./types/preferences/theme";
import type {
Expand Down Expand Up @@ -248,6 +249,29 @@ function BillingHandoffLoading({ overlay = false }: { overlay?: boolean }) {
);
}

function UserSetupError() {
return (
<div
className="flex min-h-screen flex-col items-center justify-center gap-4 p-6 text-center"
data-testid="user-setup-error"
>
<div className="max-w-md space-y-2">
<h1 className="text-xl font-semibold">Could not finish setup</h1>
<p className="text-sm text-muted-foreground">
We could not create your MCPJam user record. Refresh and try again.
</p>
</div>
<button
type="button"
className="rounded bg-primary px-4 py-2 text-sm font-medium text-primary-foreground"
onClick={() => window.location.reload()}
>
Refresh
</button>
</div>
);
}

function resolveDeletedOrganizationFallbackId(
organizations: ReadonlyArray<{ _id: string; myRole?: string }>,
): string | undefined {
Expand Down Expand Up @@ -344,6 +368,10 @@ export default function App() {
isLoading: isWorkOsLoading,
} = useAuth();
const { isAuthenticated, isLoading: isAuthLoading } = useConvexAuth();
const currentUser = useQuery(
"users:getCurrentUser" as any,
isAuthenticated ? ({} as any) : "skip",
);
const [hostedOAuthHandling, setHostedOAuthHandling] = useState(() => {
if (!HOSTED_MODE) {
return false;
Expand Down Expand Up @@ -596,7 +624,7 @@ export default function App() {
// Set up Electron OAuth callback handling
useElectronOAuth();
// Ensure a `users` row exists after Convex auth
useEnsureDbUser();
const { isEnsuringUser } = useEnsureDbUser();

const isDebugCallback = window.location.pathname.startsWith(
"/oauth/callback/debug",
Expand Down Expand Up @@ -1792,6 +1820,32 @@ export default function App() {
return <LoadingScreen />;
}

if (
!isHostedChatRoute &&
isAuthenticated &&
(currentUser === undefined || (currentUser === null && isEnsuringUser))
) {
return <LoadingScreen />;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (!isHostedChatRoute && isAuthenticated && currentUser === null) {
return <UserSetupError />;
}

if (
!isHostedChatRoute &&
isAuthenticated &&
currentUser?.occupationRequired === true &&
!currentUser?.occupation?.trim()
) {
return (
<OccupationGate
userId={workOsUser?.id ?? null}
email={workOsUser?.email}
/>
);
}

const shouldShowActiveServerSelector =
activeTab === "tools" ||
activeTab === "resources" ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@
}));

vi.mock("../hooks/useEnsureDbUser", () => ({
useEnsureDbUser: vi.fn(),
useEnsureDbUser: vi.fn(() => ({ isEnsuringUser: false })),
}));

vi.mock("../hooks/usePostHogIdentify", () => ({
Expand Down Expand Up @@ -578,7 +578,7 @@
expect(
screen.queryByTestId("hosted-oauth-loading")
).not.toBeInTheDocument();
expect(screen.getByText("Servers Tab")).toBeInTheDocument();

Check failure on line 581 in mcpjam-inspector/client/src/__tests__/App.hosted-oauth.test.tsx

View workflow job for this annotation

GitHub Actions / Run Tests

src/__tests__/App.hosted-oauth.test.tsx > App hosted OAuth callback handling > does not keep the hosted loading screen for workspace OAuth callbacks

TestingLibraryElementError: Unable to find an element with the text: Servers Tab. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body> <div> <div class="flex min-h-screen flex-col items-center justify-center gap-4 p-6 text-center" data-testid="user-setup-error" > <div class="max-w-md space-y-2" > <h1 class="text-xl font-semibold" > Could not finish setup </h1> <p class="text-sm text-muted-foreground" > We could not create your MCPJam user record. Refresh and try again. </p> </div> <button class="rounded bg-primary px-4 py-2 text-sm font-medium text-primary-foreground" type="button" > Refresh </button> </div> </div> </body> ❯ Object.getElementError ../../node_modules/@testing-library/dom/dist/config.js:37:19 ❯ ../../node_modules/@testing-library/dom/dist/query-helpers.js:76:38 ❯ ../../node_modules/@testing-library/dom/dist/query-helpers.js:52:17 ❯ ../../node_modules/@testing-library/dom/dist/query-helpers.js:95:19 ❯ src/__tests__/App.hosted-oauth.test.tsx:581:19

await waitFor(() => {
expect(mockCompleteHostedOAuthCallback).not.toHaveBeenCalled();
Expand Down Expand Up @@ -880,7 +880,7 @@
render(<App />);

await waitFor(() => {
expect(mockChatboxesTab).toHaveBeenCalled();

Check failure on line 883 in mcpjam-inspector/client/src/__tests__/App.hosted-oauth.test.tsx

View workflow job for this annotation

GitHub Actions / Run Tests

src/__tests__/App.hosted-oauth.test.tsx > App hosted OAuth callback handling > passes a billing-safe workspace id to the chatboxes tab

AssertionError: expected "spy" to be called at least once Ignored nodes: comments, script, style <html> <head /> <body> <div> <div data-testid="hosted-oauth-loading" /> </div> </body> </html> ❯ src/__tests__/App.hosted-oauth.test.tsx:883:32 ❯ runWithExpensiveErrorDiagnosticsDisabled ../../node_modules/@testing-library/dom/dist/config.js:47:12 ❯ checkCallback ../../node_modules/@testing-library/dom/dist/wait-for.js:124:77 ❯ Timeout.checkRealTimersCallback ../../node_modules/@testing-library/dom/dist/wait-for.js:118:16
});

const lastCall =
Expand Down Expand Up @@ -1033,7 +1033,7 @@
render(<App />);

await waitFor(() => {
expect(mockMCPSidebar).toHaveBeenCalled();

Check failure on line 1036 in mcpjam-inspector/client/src/__tests__/App.hosted-oauth.test.tsx

View workflow job for this annotation

GitHub Actions / Run Tests

src/__tests__/App.hosted-oauth.test.tsx > App hosted OAuth callback handling > keeps the sidebar-selected org active when navigating back to servers

AssertionError: expected "spy" to be called at least once Ignored nodes: comments, script, style <html> <head /> <body> <div> <div data-testid="hosted-oauth-loading" /> </div> </body> </html> ❯ src/__tests__/App.hosted-oauth.test.tsx:1036:30 ❯ runWithExpensiveErrorDiagnosticsDisabled ../../node_modules/@testing-library/dom/dist/config.js:47:12 ❯ checkCallback ../../node_modules/@testing-library/dom/dist/wait-for.js:124:77 ❯ Timeout.checkRealTimersCallback ../../node_modules/@testing-library/dom/dist/wait-for.js:118:16
});

const getLastSidebarProps = () =>
Expand Down Expand Up @@ -1111,7 +1111,7 @@
render(<App />);

await waitFor(() => {
expect(mockMCPSidebar).toHaveBeenCalled();

Check failure on line 1114 in mcpjam-inspector/client/src/__tests__/App.hosted-oauth.test.tsx

View workflow job for this annotation

GitHub Actions / Run Tests

src/__tests__/App.hosted-oauth.test.tsx > App hosted OAuth callback handling > preserves the newly selected org when navigating away immediately

AssertionError: expected "spy" to be called at least once Ignored nodes: comments, script, style <html> <head /> <body> <div> <div data-testid="hosted-oauth-loading" /> </div> </body> </html> ❯ src/__tests__/App.hosted-oauth.test.tsx:1114:30 ❯ runWithExpensiveErrorDiagnosticsDisabled ../../node_modules/@testing-library/dom/dist/config.js:47:12 ❯ checkCallback ../../node_modules/@testing-library/dom/dist/wait-for.js:124:77 ❯ Timeout.checkRealTimersCallback ../../node_modules/@testing-library/dom/dist/wait-for.js:118:16
});

const getLastSidebarProps = () =>
Expand Down Expand Up @@ -1225,7 +1225,7 @@
render(<App />);

await waitFor(() => {
expect(mockMCPSidebar).toHaveBeenCalled();

Check failure on line 1228 in mcpjam-inspector/client/src/__tests__/App.hosted-oauth.test.tsx

View workflow job for this annotation

GitHub Actions / Run Tests

src/__tests__/App.hosted-oauth.test.tsx > App hosted OAuth callback handling > disables sidebar workspace creation when the routed org is free and at cap

AssertionError: expected "spy" to be called at least once Ignored nodes: comments, script, style <html> <head /> <body> <div> <div data-testid="hosted-oauth-loading" /> </div> </body> </html> ❯ src/__tests__/App.hosted-oauth.test.tsx:1228:30 ❯ runWithExpensiveErrorDiagnosticsDisabled ../../node_modules/@testing-library/dom/dist/config.js:47:12 ❯ checkCallback ../../node_modules/@testing-library/dom/dist/wait-for.js:124:77 ❯ Timeout.checkRealTimersCallback ../../node_modules/@testing-library/dom/dist/wait-for.js:118:16
});

const lastCall =
Expand Down Expand Up @@ -1304,7 +1304,7 @@

render(<App />);

await waitFor(() => {

Check failure on line 1307 in mcpjam-inspector/client/src/__tests__/App.hosted-oauth.test.tsx

View workflow job for this annotation

GitHub Actions / Run Tests

src/__tests__/App.hosted-oauth.test.tsx > App hosted OAuth callback handling > restores the billing callback back into the billing flow when session intent exists

TestingLibraryElementError: Unable to find an element by: [data-testid="billing-handoff-overlay"] Ignored nodes: comments, script, style <body> <div> <div data-testid="hosted-oauth-loading" /> </div> </body> Ignored nodes: comments, script, style <html> <head /> <body> <div> <div data-testid="hosted-oauth-loading" /> </div> </body> </html> ❯ Proxy.waitForWrapper ../../node_modules/@testing-library/dom/dist/wait-for.js:163:27 ❯ src/__tests__/App.hosted-oauth.test.tsx:1307:11
expect(replaceStateSpy).toHaveBeenCalledWith({}, "", "/billing");
expect(screen.getByTestId("billing-handoff-overlay")).toBeInTheDocument();
});
Expand All @@ -1322,7 +1322,7 @@

render(<App />);

await waitFor(() => {

Check failure on line 1325 in mcpjam-inspector/client/src/__tests__/App.hosted-oauth.test.tsx

View workflow job for this annotation

GitHub Actions / Run Tests

src/__tests__/App.hosted-oauth.test.tsx > App hosted OAuth callback handling > falls back to the default callback destination when billing session intent is missing

TestingLibraryElementError: Unable to find an element with the text: Servers Tab. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body> <div> <div class="flex min-h-screen flex-col items-center justify-center gap-4 p-6 text-center" data-testid="user-setup-error" > <div class="max-w-md space-y-2" > <h1 class="text-xl font-semibold" > Could not finish setup </h1> <p class="text-sm text-muted-foreground" > We could not create your MCPJam user record. Refresh and try again. </p> </div> <button class="rounded bg-primary px-4 py-2 text-sm font-medium text-primary-foreground" type="button" > Refresh </button> </div> </div> </body> Ignored nodes: comments, script, style <html> <head /> <body> <div> <div class="flex min-h-screen flex-col items-center justify-center gap-4 p-6 text-center" data-testid="user-setup-error" > <div class="max-w-md space-y-2" > <h1 class="text-xl font-semibold" > Could not finish setup </h1> <p class="text-sm text-muted-foreground" > We could not create your MCPJam user record. Refresh and try again. </p> </div> <button class="rounded bg-primary px-4 py-2 text-sm font-medium text-primary-foreground" type="button" > Refresh </button> </div> </div> </body> </html> ❯ Proxy.waitForWrapper ../../node_modules/@testing-library/dom/dist/wait-for.js:163:27 ❯ src/__tests__/App.hosted-oauth.test.tsx:1325:11
expect(replaceStateSpy).toHaveBeenCalledWith({}, "", "/");
expect(screen.getByText("Servers Tab")).toBeInTheDocument();
});
Expand Down Expand Up @@ -1358,7 +1358,7 @@

render(<App />);

await waitFor(() => {

Check failure on line 1361 in mcpjam-inspector/client/src/__tests__/App.hosted-oauth.test.tsx

View workflow job for this annotation

GitHub Actions / Run Tests

src/__tests__/App.hosted-oauth.test.tsx > App hosted OAuth callback handling > keeps a persisted billing resume alive when /billing returns without query params

TestingLibraryElementError: Unable to find an element by: [data-testid="billing-handoff-overlay"] Ignored nodes: comments, script, style <body> <div> <div data-testid="hosted-oauth-loading" /> </div> </body> Ignored nodes: comments, script, style <html> <head /> <body> <div> <div data-testid="hosted-oauth-loading" /> </div> </body> </html> ❯ Proxy.waitForWrapper ../../node_modules/@testing-library/dom/dist/wait-for.js:163:27 ❯ src/__tests__/App.hosted-oauth.test.tsx:1361:11
expect(screen.getByTestId("billing-handoff-overlay")).toBeInTheDocument();
expect(mockOrganizationsTab).toHaveBeenCalled();
});
Expand Down Expand Up @@ -1440,7 +1440,7 @@

render(<App />);

expect(screen.getByText("Preparing checkout...")).toBeInTheDocument();

Check failure on line 1443 in mcpjam-inspector/client/src/__tests__/App.hosted-oauth.test.tsx

View workflow job for this annotation

GitHub Actions / Run Tests

src/__tests__/App.hosted-oauth.test.tsx > App hosted OAuth callback handling > keeps billing resume behind the checkout spinner for signed-in users

TestingLibraryElementError: Unable to find an element with the text: Preparing checkout.... This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body> <div> <div data-testid="hosted-oauth-loading" /> </div> </body> ❯ Object.getElementError ../../node_modules/@testing-library/dom/dist/config.js:37:19 ❯ ../../node_modules/@testing-library/dom/dist/query-helpers.js:76:38 ❯ ../../node_modules/@testing-library/dom/dist/query-helpers.js:52:17 ❯ ../../node_modules/@testing-library/dom/dist/query-helpers.js:95:19 ❯ src/__tests__/App.hosted-oauth.test.tsx:1443:19

await waitFor(() => {
expect(screen.getByTestId("billing-handoff-overlay")).toBeInTheDocument();
Expand Down Expand Up @@ -1513,7 +1513,7 @@

render(<App />);

await waitFor(() => {

Check failure on line 1516 in mcpjam-inspector/client/src/__tests__/App.hosted-oauth.test.tsx

View workflow job for this annotation

GitHub Actions / Run Tests

src/__tests__/App.hosted-oauth.test.tsx > App hosted OAuth callback handling > drops the billing overlay when checkout intent is consumed

TestingLibraryElementError: Unable to find an element by: [data-testid="billing-handoff-overlay"] Ignored nodes: comments, script, style <body> <div> <div data-testid="hosted-oauth-loading" /> </div> </body> Ignored nodes: comments, script, style <html> <head /> <body> <div> <div data-testid="hosted-oauth-loading" /> </div> </body> </html> ❯ Proxy.waitForWrapper ../../node_modules/@testing-library/dom/dist/wait-for.js:163:27 ❯ src/__tests__/App.hosted-oauth.test.tsx:1516:11
expect(screen.getByTestId("billing-handoff-overlay")).toBeInTheDocument();
expect(screen.getByTestId("consume-checkout-intent")).toBeInTheDocument();
});
Expand Down
143 changes: 143 additions & 0 deletions mcpjam-inspector/client/src/components/signup/OccupationGate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { FormEvent, useState } from "react";
import { useMutation } from "convex/react";
import { usePostHog } from "posthog-js/react";
import { ArrowRight, Check, Loader2 } from "lucide-react";
import { Button } from "@mcpjam/design-system/button";
import { Input } from "@mcpjam/design-system/input";
import { standardEventProps } from "@/lib/PosthogUtils";
import { cn } from "@/lib/utils";

const OCCUPATION_SUGGESTIONS = [
"Software Engineer",
"Product Manager",
"Engineering Manager",
"Platform Engineer",
"Other",
];

interface OccupationGateProps {
userId?: string | null;
email?: string | null;
}

export function OccupationGate({ userId, email }: OccupationGateProps) {
const posthog = usePostHog();
const updateOccupation = useMutation("users:updateOccupation" as any);
const [occupation, setOccupation] = useState("");
const [error, setError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);

const trimmedOccupation = occupation.trim();
const canSubmit = trimmedOccupation.length > 0 && !isSubmitting;

const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!trimmedOccupation) {
setError("Enter your role to continue.");
return;
}

setIsSubmitting(true);
setError(null);

try {
await updateOccupation({ occupation: trimmedOccupation });
posthog.setPersonProperties({ occupation: trimmedOccupation });
posthog.capture("signup_occupation_submitted", {
...standardEventProps("signup_occupation_gate"),
occupation: trimmedOccupation,
});
posthog.register({ occupation: trimmedOccupation });
} catch (err) {
console.error("[signup] Failed to save occupation", err);
setError("Could not save your occupation. Please try again.");
} finally {
setIsSubmitting(false);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
};

return (
<main className="min-h-screen bg-background text-foreground">
<div className="mx-auto flex min-h-screen w-full max-w-lg flex-col justify-center px-6 py-12">
<div className="mb-10">
<img src="/mcp_jam.svg" alt="MCPJam" className="mb-8 h-9 w-auto" />
<h1 className="text-3xl font-semibold tracking-normal">
What is your role?
</h1>
<p className="mt-3 text-sm text-muted-foreground">
This helps us understand who is using MCPJam.
</p>
</div>

<form onSubmit={handleSubmit}>
<Input
value={occupation}
onChange={(event) => {
setOccupation(event.target.value);
if (error) setError(null);
}}
placeholder="Type your role"
autoComplete="organization-title"
className="mb-4 h-11 text-base md:text-sm"
autoFocus
/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

<p className="mb-3 text-xs font-semibold uppercase tracking-widest text-muted-foreground">
Suggestions
</p>

<div className="flex flex-wrap gap-2">
{OCCUPATION_SUGGESTIONS.map((suggestion) => {
const isSelected =
occupation.trim().toLowerCase() === suggestion.toLowerCase();
return (
<button
key={suggestion}
type="button"
onClick={() => {
setOccupation(suggestion);
if (error) setError(null);
}}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
className={cn(
"flex items-center gap-1.5 rounded-full border px-4 py-2 text-sm font-medium transition-all duration-150",
isSelected
? "border-primary bg-primary text-primary-foreground shadow-sm"
: "border-border bg-background text-foreground hover:-translate-y-px hover:border-primary hover:bg-primary hover:text-primary-foreground hover:shadow-md",
)}
>
{isSelected && <Check className="h-3.5 w-3.5" />}
{suggestion}
</button>
);
})}
</div>

{error ? (
<p className="mt-3 text-sm text-destructive">{error}</p>
) : null}

<hr className="my-6 border-border" />

<Button type="submit" disabled={!canSubmit} className="w-full">
{isSubmitting ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<ArrowRight className="h-4 w-4" />
)}
Continue
</Button>
</form>

{email ? (
<p className="mt-6 text-xs text-muted-foreground">
Signed in as {email}
</p>
) : userId ? (
<p className="mt-6 text-xs text-muted-foreground">
Signed in to MCPJam
</p>
) : null}
</div>
</main>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const mockState = vi.hoisted(() => ({
convexAuth: {
isAuthenticated: false,
},
convexUser: null as { occupation?: string } | null,
detectPlatform: vi.fn(() => "mac"),
}));

Expand All @@ -32,6 +33,7 @@ vi.mock("@workos-inc/authkit-react", () => ({

vi.mock("convex/react", () => ({
useConvexAuth: () => mockState.convexAuth,
useQuery: () => mockState.convexUser,
}));

vi.mock("@/lib/PosthogUtils", () => ({
Expand All @@ -44,6 +46,7 @@ describe("usePostHogIdentify", () => {
vi.stubGlobal("__APP_VERSION__", "2.0.13-test");
mockState.auth.user = null;
mockState.convexAuth.isAuthenticated = false;
mockState.convexUser = null;
mockState.detectPlatform.mockReturnValue("mac");
});

Expand Down Expand Up @@ -114,4 +117,25 @@ describe("usePostHogIdentify", () => {
});
expect(mockState.posthog.identify).not.toHaveBeenCalled();
});

it("adds occupation when the Convex user has one", () => {
mockState.auth.user = {
id: "user_123",
email: "user@example.com",
firstName: "Taylor",
lastName: "Smith",
};
mockState.convexAuth.isAuthenticated = true;
mockState.convexUser = { occupation: "Platform Engineer" };

renderHook(() => usePostHogIdentify());

expect(mockState.posthog.identify).toHaveBeenCalledWith("user_123", {
email: "user@example.com",
name: "Taylor Smith",
first_name: "Taylor",
last_name: "Smith",
occupation: "Platform Engineer",
});
});
});
25 changes: 20 additions & 5 deletions mcpjam-inspector/client/src/hooks/useEnsureDbUser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import { useMutation, useConvexAuth } from "convex/react";
import { useAuth } from "@workos-inc/authkit-react";
import * as Sentry from "@sentry/react";
Expand All @@ -13,25 +13,35 @@ export function useEnsureDbUser() {
const { isAuthenticated, isLoading } = useConvexAuth();
const ensureUser = useMutation("users:ensureUser" as any);
const lastEnsuredUserIdRef = useRef<string | null>(null);
const [isEnsuringUser, setIsEnsuringUser] = useState(false);

// Reset cache on logout so we re-run for the next login in the same session
useEffect(() => {
if (!isAuthenticated) {
lastEnsuredUserIdRef.current = null;
setIsEnsuringUser(false);
Sentry.setUser(null); // Clear Sentry user on logout
}
}, [isAuthenticated]);

useEffect(() => {
if (isLoading) return;
if (isLoading) {
return;
}
// WorkOS user hydration can briefly lead Convex auth. This is expected
// during callback completion; wait for isAuthenticated instead of throwing.
if (!isAuthenticated) return;
if (!user) return;
if (!isAuthenticated || !user) {
setIsEnsuringUser(false);
return;
}

// Only (re)ensure when the authenticated WorkOS user changes.
if (lastEnsuredUserIdRef.current === user.id) return;
if (lastEnsuredUserIdRef.current === user.id) {
setIsEnsuringUser(false);
return;
}

setIsEnsuringUser(true);
ensureUser()
.then((id: string | null) => {
// eslint-disable-next-line no-console
Expand All @@ -44,6 +54,11 @@ export function useEnsureDbUser() {
console.error("[auth] ensureUser failed", err);
// allow retry next effect pass
lastEnsuredUserIdRef.current = null;
})
.finally(() => {
setIsEnsuringUser(false);
});
}, [isAuthenticated, isLoading, user, ensureUser]);

return { isEnsuringUser };
}
Loading
Loading