-
Notifications
You must be signed in to change notification settings - Fork 1
Fix Convex Auth UI Status #157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| ``` | ||
|
|
||
| ## 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 | ||
| 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> | ||
| ); | ||
| } |
| 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 { | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a user signs in, this effect schedules a 500ms timeout to call 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]"> | ||
|
|
@@ -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> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
textor leaving it empty for plain text diagrams.Apply this diff:
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
161-161: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents