From 78fde782b9b7634c9cab7902573363d99756caf7 Mon Sep 17 00:00:00 2001 From: Vincent Grobler Date: Thu, 23 Apr 2026 13:12:54 +0100 Subject: [PATCH] fix: graceful auth callback for PKCE timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When email verification link opens in a different browser context, the PKCE code verifier is missing and onAuthStateChange never fires. Before: showed 'Authentication timed out' error with a plain text link. After: - First checks if user is already authenticated → auto-redirects - If not, shows 'Email verified!' success screen with branded Sign In button - Error state with proper icon and messaging as fallback - Uses brand-primary styled buttons and lucide icons --- src/pages/AuthCallback.tsx | 82 ++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 12 deletions(-) diff --git a/src/pages/AuthCallback.tsx b/src/pages/AuthCallback.tsx index d090550..9a2532f 100644 --- a/src/pages/AuthCallback.tsx +++ b/src/pages/AuthCallback.tsx @@ -4,34 +4,61 @@ import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import { supabase } from '@/lib/supabase' +import { CheckCircle2, AlertCircle, Loader2 } from 'lucide-react' /** * AuthCallback handles the redirect from OAuth providers and email * confirmation links. Supabase appends tokens as URL hash fragments — * this page exchanges them for a session via onAuthStateChange. + * + * PKCE flow: when the email verification link opens in a different browser + * context (email app webview, different browser), the code verifier stored + * in sessionStorage is missing. In that case, onAuthStateChange never fires + * SIGNED_IN. We handle this gracefully by: + * 1. Checking if the user is already authenticated (e.g. already signed in) + * 2. If not, showing a success message explaining the email was verified + * and they can sign in from their original browser/tab. */ export function AuthCallback() { const navigate = useNavigate() - const [error, setError] = useState(null) + const [status, setStatus] = useState<'loading' | 'verified' | 'error'>('loading') + const [errorMessage, setErrorMessage] = useState(null) useEffect(() => { const { data: { subscription }, } = supabase.auth.onAuthStateChange((event, session) => { if (event === 'SIGNED_IN' && session) { - // Check for a pending redirect (e.g. invite link) saved by Auth page + // Successful token exchange — redirect to app const pendingRedirect = sessionStorage.getItem('crewform:authRedirect') sessionStorage.removeItem('crewform:authRedirect') navigate(pendingRedirect ?? '/', { replace: true }) } else if (event === 'PASSWORD_RECOVERY') { - // User clicked a password reset link — redirect to reset form navigate('/auth/reset-password', { replace: true }) } }) - // Fallback: if no auth event fires within 5s, show error + // Fallback: if no auth event fires within 5s, check if user is already signed in const timeout = setTimeout(() => { - setError('Authentication timed out. Please try again.') + void (async () => { + try { + const { data: { session } } = await supabase.auth.getSession() + + if (session) { + // User is already authenticated — just redirect + const pendingRedirect = sessionStorage.getItem('crewform:authRedirect') + sessionStorage.removeItem('crewform:authRedirect') + navigate(pendingRedirect ?? '/', { replace: true }) + } else { + // No session — likely opened verification link in different browser. + // The email IS verified, they just need to sign in. + setStatus('verified') + } + } catch { + setErrorMessage('Something went wrong. Please try signing in.') + setStatus('error') + } + })() }, 5000) return () => { @@ -40,31 +67,62 @@ export function AuthCallback() { } }, [navigate]) - if (error) { + // ─── Verified state — email confirmed, prompt to sign in ──────────── + if (status === 'verified') { return (
-
-
- {error} +
+
+
+

Email verified!

+

+ Your email has been confirmed. Sign in to get started with CrewForm. +

) } + // ─── Error state ──────────────────────────────────────────────────── + if (status === 'error') { + return ( +
+
+
+ +
+

Something went wrong

+

+ {errorMessage ?? 'Authentication failed. Please try signing in again.'} +

+ +
+
+ ) + } + + // ─── Loading state ────────────────────────────────────────────────── return (
-
+ Completing sign in...
) } +