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
212 changes: 212 additions & 0 deletions explanations/CONVEX_AUTH_UI_FIX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# Convex Auth UI Status Fix

## Problem
After signing in with Convex Auth, the UI did not update to show the signed-in status (no "Welcome" message, navbar didn't update to show user control instead of sign-in button, etc.). However, no errors appeared in Chrome DevTools console or Vercel logs.

## Root Causes Identified

### 1. **Race Condition in `useUser()` Hook**
The original `useUser()` hook had a critical race condition:

```typescript
// BEFORE - PROBLEMATIC
if (isLoading) {
return null;
}
```

This would return `null` during the auth loading state, which could be interpreted by components as "user not authenticated" instead of "still loading".

### 2. **Missing Re-render Trigger**
The hook checked both `isAuthenticated` and `userData`, but wasn't properly handling the async nature of Convex Auth. When a user signed in:
1. Convex Auth updated the auth state
2. But the `useQuery(api.users.getCurrentUser)` might not re-run with the new auth context immediately
3. This caused the navbar to still show "Sign in" button even though `isAuthenticated` was true

### 3. **Modal Close Timing**
The auth modal detection relied on comparing previous and current user state, but without proper cleanup or timing, the state comparison could fail to trigger the modal close.

## Fixes Applied

### Fix 1: Updated `useUser()` Hook (`src/lib/auth-client.ts`)

**Key Changes:**
1. Properly distinguish between "loading" and "not authenticated"
2. Re-apply `authIsLoading` check first to avoid premature returns
3. Added explicit null checks with clear comments explaining the logic
4. Ensured query is always called so it responds to auth context changes

```typescript
export function useUser() {
const { isAuthenticated, isLoading: authIsLoading } = useConvexAuthBase();
const { signOut } = useAuthActions();
const userData = useQuery(api.users.getCurrentUser);

// While auth is still loading, return null
if (authIsLoading) {
return null;
}

// If not authenticated, return null
if (!isAuthenticated) {
return null;
}

// If authenticated but user data is still loading (undefined), return null
if (userData === undefined) {
return null;
}

// If no user data found (null response), return null
if (userData === null) {
return null;
}

// Return the user object
return { ... };
}
```

**Why This Works:**
- Properly sequences the checks: loading → authenticated → data available
- Allows React to trigger re-renders when `userData` changes from undefined to actual data
- Components using `useUser()` will automatically update when auth state changes

### Fix 2: Enhanced Auth Modal (`src/components/auth-modal.tsx`)

**Key Changes:**
1. Added `useRef` to prevent duplicate toast notifications
2. Added slight delay before closing modal to ensure UI has time to update
3. Reset toast flag when modal is opened

```typescript
const hasShownToastRef = useRef(false);

useEffect(() => {
if (!previousUser && user) {
if (!hasShownToastRef.current) {
toast.success("Welcome back!");
hasShownToastRef.current = true;
}
// Delay to ensure UI updates
const timer = setTimeout(() => {
onClose();
hasShownToastRef.current = false;
}, 500);
return () => clearTimeout(timer);
}
setPreviousUser(user);
}, [user, previousUser, onClose]);
```

**Why This Works:**
- Prevents race conditions where modal closes before UI updates
- Ensures proper state transitions in React
- Better user experience with visible confirmation

### Fix 3: Clarified `ConvexClientProvider` (`src/components/convex-provider.tsx`)

Added comments clarifying that the Convex client handles auth state changes automatically through the `ConvexAuthProvider`.

## Verification Steps

To verify the fix works:

1. **Sign In Flow:**
- Open app and click "Sign in"
- Complete OAuth or email authentication
- Verify:
- Auth modal closes
- "Welcome back!" toast appears
- Navbar shows user avatar/name instead of "Sign in" button

2. **Debug Component (Optional):**
- Import and add the debug component to your layout:
```tsx
import { AuthDebug } from "@/components/auth-debug";
// Add <AuthDebug /> to see real-time auth state
```
- After sign-in, verify:
- `isAuthenticated` = `true`
- `useUser()` returns user object
- API query shows user data

3. **Network Tab:**
- Check that `/convex/CurrentUser` query is called after sign-in
- Verify it returns user data (not error)

## Testing Different Auth Providers

Test with all configured providers:
- GitHub OAuth
- Google OAuth
- Email (Resend)

## Files Modified

1. `src/lib/auth-client.ts` - Fixed useUser() hook logic
2. `src/components/auth-modal.tsx` - Enhanced modal closing behavior
3. `src/components/convex-provider.tsx` - Clarified comments
4. `src/components/auth-debug.tsx` - New debug component (optional)

## Related Components

These components depend on the fixes and should now work correctly:
- `src/modules/home/ui/components/navbar.tsx` - Will show user control after sign-in
- `src/components/user-control.tsx` - User dropdown menu
- `src/app/dashboard/*` - Protected routes

## Architecture Overview

```
Sign-in Flow:
1. User clicks "Sign in" → AuthModal opens
2. SignInForm calls signIn(provider)
3. Convex Auth handles OAuth/email flow
4. Browser redirected to auth callback
5. Auth state updated in ConvexAuthProvider
6. useConvexAuth() detects authenticated = true
7. useUser() hook triggers re-render
8. userData query fetches user information
9. useUser() now returns user object
10. Navbar and other components re-render
11. AuthModal detects user change and closes
12. "Welcome back!" toast shows
13. User sees navbar with their avatar
```

Comment on lines +161 to +177
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add language identifier to fenced code block.

The code block displaying the sign-in flow architecture should have a language identifier for proper rendering. Consider using text or leaving it empty for plain text diagrams.

Apply this diff:

-```
+```text
 Sign-in Flow:
 1. User clicks "Sign in" → AuthModal opens
 ...
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

161-161: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In explanations/CONVEX_AUTH_UI_FIX.md around lines 161 to 177, the fenced code
block showing the sign-in flow is missing a language identifier; update the
opening fence from ``` to ```text (or another plain text identifier) and keep
the closing ``` as-is so the block renders correctly as plain text in markdown.

## Debugging Tips

If issues persist after these fixes:

1. **Check Environment Variables:**
```bash
NEXT_PUBLIC_CONVEX_URL
AUTH_GITHUB_ID & AUTH_GITHUB_SECRET (for GitHub)
AUTH_GOOGLE_ID & AUTH_GOOGLE_SECRET (for Google)
AUTH_RESEND_KEY & AUTH_EMAIL_FROM (for Email)
```

2. **Check Browser DevTools:**
- Network tab: Look for `convex/getCurrentUser` queries
- Application tab: Check `__convex_auth` cookie is present after sign-in
- Console: Check for any auth-related errors

3. **Check Convex Logs:**
- Run `bun run convex:dev` to see Convex backend logs
- Look for auth context changes and query execution

4. **Add Console Debugging:**
```typescript
// In useUser() for debugging
console.log('Auth state:', { isAuthenticated, authIsLoading });
console.log('User data:', userData);
```

## Future Improvements

Consider these enhancements:
1. Add auth state persistence check
2. Implement auth refresh mechanism if tokens expire
3. Add better error boundaries for auth failures
4. Implement auth state recovery on network errors
72 changes: 72 additions & 0 deletions src/components/auth-debug.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"use client";

import { useConvexAuth } from "convex/react";
import { useAuthActions } from "@convex-dev/auth/react";
import { useUser } from "@/lib/auth-client";
import { useQuery } from "convex/react";
import { api } from "@/convex/_generated/api";

/**
* Debug component to help diagnose auth issues
* Add this temporarily to debug authentication problems
*
* Usage:
* import { AuthDebug } from "@/components/auth-debug";
* // Add <AuthDebug /> somewhere in your component tree to see debug info
*/
export function AuthDebug() {
const convexAuth = useConvexAuth();
const user = useUser();
const userData = useQuery(api.users.getCurrentUser);

return (
<div className="fixed bottom-0 right-0 bg-black text-white p-4 text-xs font-mono max-w-md overflow-auto max-h-64 z-50 rounded-tl">
<div className="space-y-2">
<div className="font-bold text-blue-400">Auth Debug Info</div>

<div>
<div className="text-green-400">Convex Auth State:</div>
<div className="pl-2">
isAuthenticated: <span className="text-yellow-400">{String(convexAuth.isAuthenticated)}</span>
</div>
<div className="pl-2">
isLoading: <span className="text-yellow-400">{String(convexAuth.isLoading)}</span>
</div>
</div>

<div>
<div className="text-green-400">User Data:</div>
<div className="pl-2">
useUser() result: <span className="text-yellow-400">{user ? "User object" : "null"}</span>
</div>
{user && (
<>
<div className="pl-2">
email: <span className="text-yellow-400">{user.email}</span>
</div>
<div className="pl-2">
name: <span className="text-yellow-400">{user.name}</span>
</div>
</>
)}
</div>

<div>
<div className="text-green-400">Query Result:</div>
<div className="pl-2">
api.users.getCurrentUser: <span className="text-yellow-400">{userData === undefined ? "loading" : userData === null ? "null" : "data"}</span>
</div>
</div>

<div className="text-gray-400 border-t border-gray-600 pt-2 mt-2">
After sign-in:
<ul className="list-disc pl-5 text-gray-300">
<li>isAuthenticated should be true</li>
<li>useUser() should return user object</li>
<li>API query should return user data</li>
</ul>
</div>
</div>
</div>
);
}
26 changes: 21 additions & 5 deletions src/components/auth-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useEffect, useState } from "react";
import { useEffect, useState, useRef } from "react";
import { useUser } from "@/lib/auth-client";
import { SignInForm } from "@/components/auth/sign-in-form";
import {
Expand All @@ -21,17 +21,33 @@ interface AuthModalProps {
export function AuthModal({ isOpen, onClose, mode }: AuthModalProps) {
const user = useUser();
const [previousUser, setPreviousUser] = useState(user);
const hasShownToastRef = useRef(false);

// Auto-close modal when user successfully signs in
useEffect(() => {
if (!previousUser && user) {
// User just signed in
toast.success("Welcome back!");
onClose();
if (!hasShownToastRef.current) {
toast.success("Welcome back!");
hasShownToastRef.current = true;
}
// Delay the close to ensure the UI has time to update
const timer = setTimeout(() => {
onClose();
hasShownToastRef.current = false;
}, 500);
return () => clearTimeout(timer);
}
setPreviousUser(user);
}, [user, previousUser, onClose]);
Comment on lines +38 to 42

Choose a reason for hiding this comment

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

P1 Badge Delayed close timer cleared before firing

When a user signs in, this effect schedules a 500ms timeout to call onClose, but the effect also calls setPreviousUser(user) with previousUser in the dependency array. That state update triggers an immediate re-run of the effect, so the cleanup runs and clearTimeout(timer) executes before the timeout can fire. The modal therefore never auto-closes after a successful sign-in unless some other code toggles it, defeating the intended UX improvement.

Useful? React with 👍 / 👎.


// Reset toast flag when modal is opened
useEffect(() => {
if (isOpen) {
hasShownToastRef.current = false;
}
}, [isOpen]);

return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[425px]">
Expand All @@ -40,8 +56,8 @@ export function AuthModal({ isOpen, onClose, mode }: AuthModalProps) {
{mode === "signin" ? "Sign in to ZapDev" : "Create your account"}
</DialogTitle>
<DialogDescription>
{mode === "signin"
? "Sign in to access your projects and continue building with AI"
{mode === "signin"
? "Sign in to access your projects and continue building with AI"
: "Create an account to start building web applications with AI"}
</DialogDescription>
</DialogHeader>
Expand Down
6 changes: 5 additions & 1 deletion src/components/convex-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ export function ConvexClientProvider({ children }: { children: ReactNode }) {
if (!url) {
throw new Error("NEXT_PUBLIC_CONVEX_URL environment variable is not set");
}
return new ConvexReactClient(url);
const client = new ConvexReactClient(url);

// Enable automatic re-rendering on auth state changes
// This ensures components using useConvexAuth() re-render when auth status changes
return client;
}, []);

return (
Expand Down
Loading
Loading