'use client';
/**
* InviteHashHandler
*
* Handles Supabase auth tokens that come in the URL hash fragment.
* When an invite link is clicked, Supabase may redirect with tokens in the hash
* (e.g., /#access_token=...&type=invite). This component detects these tokens
* and properly handles the auth flow, including signing out any existing session
* for invite links.
*
* Note: The actual NextAuth sign-in is handled by the /auth/complete page to ensure
* proper session initialization.
*/
import { useEffect, useState, useRef } from 'react';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { signOut as nextAuthSignOut } from 'next-auth/react';
import { supabase } from '@/lib/supabase';
export function InviteHashHandler() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const [isProcessing, setIsProcessing] = useState(false);
// Refs to prevent double-processing (React Strict Mode runs effects twice)
const isProcessingHashRef = useRef(false);
// Check for auth_complete param and redirect to dedicated auth page
// This handles magic link callbacks that were redirected here
useEffect(() => {
if (typeof window === 'undefined') return;
const authComplete = searchParams.get('auth_complete');
if (authComplete !== 'magiclink' && authComplete !== 'invite') return;
// Redirect to the auth complete page which handles NextAuth sign-in
const locale = pathname.split('/')[1] || 'en';
const welcome = searchParams.get('welcome') === 'true';
const completeUrl = `/${locale}/auth/complete?type=${authComplete}${welcome ? '&welcome=true' : ''}`;
console.log('[InviteHashHandler] Detected auth_complete, redirecting to:', completeUrl);
window.location.href = completeUrl;
}, [searchParams, pathname]);
// Handle hash fragment tokens (invite links, etc.)
useEffect(() => {
// Only run on client side
if (typeof window === 'undefined') return;
const handleHashTokens = async () => {
const hash = window.location.hash;
const fullUrl = window.location.href;
// Debug logging
console.log('[InviteHashHandler] Effect running, hash:', hash ? `"${hash.substring(0, 50)}..."` : '(empty)', 'URL:', fullUrl.substring(0, 100));
if (!hash || hash.length < 2) {
console.log('[InviteHashHandler] No hash fragment found');
return;
}
// Prevent double-processing (React Strict Mode runs effects twice)
if (isProcessingHashRef.current) {
console.log('[InviteHashHandler] Already processing hash, skipping');
return;
}
// Parse the hash fragment immediately
const params = new URLSearchParams(hash.substring(1));
const accessToken = params.get('access_token');
const refreshToken = params.get('refresh_token');
const type = params.get('type');
console.log('[InviteHashHandler] Parsed hash - type:', type, 'hasAccessToken:', !!accessToken);
// Only handle if we have tokens
if (!accessToken) {
console.log('[InviteHashHandler] No access token in hash');
return;
}
// Mark as processing immediately to prevent re-runs
isProcessingHashRef.current = true;
// Clear the hash immediately to prevent re-processing on re-renders
window.history.replaceState(null, '', pathname);
console.log('[InviteHashHandler] Detected auth tokens in hash, type:', type);
// For invite links, sign out existing session and set up new Supabase session
// Then redirect with auth_complete to let the page reload and complete NextAuth sign-in
if (type === 'invite') {
setIsProcessing(true);
try {
console.log('[InviteHashHandler] Processing invite, signing out existing session...');
// Sign out of Supabase first
await supabase.auth.signOut();
// Sign out of NextAuth (don't redirect, we'll handle that)
await nextAuthSignOut({ redirect: false });
// Set the new session with the invite tokens
console.log('[InviteHashHandler] Setting new session from invite tokens...');
const { data, error } = await supabase.auth.setSession({
access_token: accessToken,
refresh_token: refreshToken || '',
});
if (error) {
console.error('[InviteHashHandler] Error setting session:', error);
router.push('/en/auth/login?error=' + encodeURIComponent(error.message));
return;
}
console.log('[InviteHashHandler] Supabase session set successfully for:', data.user?.email);
// Mark the invite as accepted in the database
if (data.user?.email) {
try {
await fetch('/api/admin/invites/accept', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: data.user.email }),
});
} catch (e) {
console.error('[InviteHashHandler] Error marking invite accepted:', e);
}
}
// Redirect to auth complete page to handle NextAuth sign-in
// This ensures SessionProvider is properly initialized on a fresh page load
const locale = pathname.split('/')[1] || 'en';
console.log('[InviteHashHandler] Redirecting to auth/complete to finish sign-in');
window.location.href = `/${locale}/auth/complete?type=invite&welcome=true`;
} catch (error) {
console.error('[InviteHashHandler] Error processing invite:', error);
router.push('/en/auth/login?error=' + encodeURIComponent('Failed to process invite'));
} finally {
setIsProcessing(false);
}
} else if (type === 'magiclink') {
// For magic links (non-invite), set Supabase session then redirect to auth/complete
setIsProcessing(true);
try {
// Set the session
const { data, error } = await supabase.auth.setSession({
access_token: accessToken,
refresh_token: refreshToken || '',
});
if (error) {
console.error('[InviteHashHandler] Error setting magic link session:', error);
router.push('/en/auth/login?error=' + encodeURIComponent(error.message));
return;
}
console.log('[InviteHashHandler] Supabase magic link session set for:', data.user?.email);
// Redirect to auth complete page to handle NextAuth sign-in
const locale = pathname.split('/')[1] || 'en';
console.log('[InviteHashHandler] Redirecting to auth/complete to finish sign-in');
window.location.href = `/${locale}/auth/complete?type=magiclink`;
} catch (error) {
console.error('[InviteHashHandler] Error processing magic link:', error);
router.push('/en/auth/login?error=' + encodeURIComponent('Failed to process magic link'));
} finally {
setIsProcessing(false);
}
}
};
handleHashTokens();
}, [pathname, router]);
// Show a loading overlay when processing auth
if (isProcessing) {
return (
<div className="fixed inset-0 bg-background-primary/90 flex items-center justify-center z-50">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-accent-red mx-auto mb-4"></div>
<p className="text-text-primary text-lg">Signing you in...</p>
<p className="text-text-secondary text-sm mt-2">Please wait while we complete authentication.</p>
</div>
</div>
);
}
return null;
}