'use client' import { useState, useEffect } from 'react' import { useParams, useRouter } from 'next/navigation' import { useTranslations } from 'next-intl' import Link from 'next/link' import { Button } from '@/components/ui/button' import { Card } from '@/components/ui/card' import { Loader2, Building2, AlertCircle } from 'lucide-react' import { createClient } from '@/lib/supabase/client' import { useToast } from '@/components/ui/use-toast' import { getBranding } from '@/lib/branding/service' const branding = getBranding() interface InviteInfo { type: 'company' companyName?: string email: string expired: boolean alreadyHasAccount: boolean } export default function InvitePage() { const params = useParams() const router = useRouter() const { toast } = useToast() const t = useTranslations('invite') const token = params.token as string const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const [invite, setInvite] = useState(null) // Email of the currently signed-in user (if any). Used to short-circuit // the login redirect for users who are already authenticated. const [currentUserEmail, setCurrentUserEmail] = useState(null) const [isJoining, setIsJoining] = useState(false) useEffect(() => { async function loadInvite() { try { // Load invite info and current session in parallel — the page needs // both to decide which CTA to render. const supabase = createClient() const [inviteRes, sessionRes] = await Promise.all([ fetch(`/api/team/accept?token=${encodeURIComponent(token)}`), supabase.auth.getUser(), ]) const data = await inviteRes.json() if (!inviteRes.ok) { setError(data.error || t('invalid_invite')) return } setInvite(data.data) setCurrentUserEmail(sessionRes.data.user?.email ?? null) } catch { setError(t('load_failed')) } finally { setIsLoading(false) } } loadInvite() }, [token, t]) const secureCookieFlag = typeof window !== 'undefined' && window.location.protocol === 'https:' ? '; secure' : '' // True when the signed-in user's email matches the invite — in that case // we can accept the invite with a single click, no re-login required. const isLoggedInAsInvitee = !!currentUserEmail && !!invite && currentUserEmail.toLowerCase() === invite.email.toLowerCase() // True when someone is signed in but with a different email than the // invite is for. They need to sign out first. const isLoggedInAsOther = !!currentUserEmail && !!invite && !isLoggedInAsInvitee const handleAccept = () => { // Store invite token in cookie before redirecting to register document.cookie = `gnubok-invite-token=${token}; path=/; max-age=3600; samesite=lax${secureCookieFlag}` router.push(`/register?invite=${encodeURIComponent(token)}`) } const handleAcceptExistingUser = () => { // Store invite token in cookie before redirecting to login document.cookie = `gnubok-invite-token=${token}; path=/; max-age=3600; samesite=lax${secureCookieFlag}` router.push('/login') } // Already signed in as the invitee — accept directly, no login detour. // POST /api/team/accept handles the membership insert + sets the active // company; we then full-reload to '/' so middleware picks up the new // company context and the switcher shows it. const handleJoinNow = async () => { setIsJoining(true) try { const res = await fetch('/api/team/accept', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token }), }) const body = await res.json().catch(() => ({})) if (!res.ok) { toast({ title: t('join_failed_title'), description: body.error || t('unexpected_error'), variant: 'destructive', }) setIsJoining(false) return } toast({ title: t('welcome_title'), description: invite?.companyName ? t('joined_named', { companyName: invite.companyName }) : t('joined_generic'), }) // Full reload so the middleware re-resolves company context from the // updated user_preferences.active_company_id. window.location.href = '/' } catch (err) { console.error('[invite] join failed:', err) toast({ title: t('join_failed_title'), description: t('unexpected_error'), variant: 'destructive', }) setIsJoining(false) } } const handleSignOutAndRetry = async () => { const supabase = createClient() await supabase.auth.signOut() // Keep the invite cookie alive so the next login/register picks it up. document.cookie = `gnubok-invite-token=${token}; path=/; max-age=3600; samesite=lax${secureCookieFlag}` if (invite?.alreadyHasAccount) { router.push('/login') } else { router.push(`/register?invite=${encodeURIComponent(token)}`) } } if (isLoading) { return (
) } return (
{branding.appName.toLowerCase()}

{error ? t('header_invalid') : t('header_invited')}

{error ? (

{error}

{t('contact_inviter')}

{t('go_to_login')}
) : invite?.expired ? (

{t('expired_title')}

{t('expired_description')}

) : isLoggedInAsInvitee ? ( // Already signed in as the invitee — one-click join. // Prioritized over alreadyHasAccount to avoid the broken flow // where a false-negative from the email check would send a // logged-in user to /register, which middleware bounces to /.

{invite.companyName}

{t('invited_to_company')}

{t.rich('logged_in_as', { email: currentUserEmail ?? '', strong: (chunks) => {chunks}, })}

) : isLoggedInAsOther ? ( // Signed in as a different user — ask them to sign out first.

{invite.companyName}

{t.rich('wrong_account', { invitedEmail: invite.email, currentEmail: currentUserEmail ?? '', strong: (chunks) => {chunks}, })}

{t('signout_then_login')}

) : invite?.alreadyHasAccount ? ( // Not signed in — email has an existing account, bounce to login.

{invite.companyName}

{t('invited_to_company')}

{t.rich('existing_account', { email: invite.email, appName: branding.appName.toLowerCase(), strong: (chunks) => {chunks}, })}

) : invite ? ( // Not signed in, no existing account — register.

{invite.companyName}

{t('invited_to_company')}

{t('terms_notice')}

) : null}
) }