'use client'; import {useCallback, useState} from 'react'; import {useRouter, useSearchParams} from 'next/navigation'; import {useSession} from '@/features/auth/providers/SessionProvider'; import {authRepository} from '@/features/auth/repositories/authRepository'; import {destinationForUser} from '@/features/auth/services/roleDestination'; import type {InvitationPreview} from '@/features/auth/types/join'; /** The platform's own floor, checked here too so the common mistake costs no round trip. */ export const MIN_PASSWORD_LENGTH = 8; /** * Redeeming an invitation, in two steps. * * 1. The code is checked BEFORE anybody chooses a password, so the screen * can say what they are joining and a mistyped code is caught first. * 2. Name and password are sent with the code; the answer is a full session * and the person goes straight into the console — never to a login form. * * A code in the link (`/join?code=…`) is prefilled, so a code sent over * WhatsApp is one tap. Dashes and case in a code do not matter upstream; it is * sent exactly as typed. */ export function useJoinForm() { const router = useRouter(); const searchParams = useSearchParams(); const {refresh} = useSession(); const [code, setCode] = useState(() => searchParams.get('code') ?? ''); const [preview, setPreview] = useState(null); const [fullName, setFullName] = useState(''); const [password, setPassword] = useState(''); const [confirm, setConfirm] = useState(''); const [error, setError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const check = useCallback( async (value: string) => { const trimmed = value.trim(); if (!trimmed) { setError('Enter the invitation code you were given.'); return; } setIsSubmitting(true); setError(null); const res = await authRepository.invitation(trimmed); setIsSubmitting(false); if (!res.ok || !res.data) { setError(res.message ?? 'That invitation code is not valid. Ask for a new one.'); return; } setPreview(res.data); // Prefilled from the invitation, and still editable: it is how they // appear to colleagues, and whoever invited them may have guessed. setFullName((prev) => prev || res.data!.fullName); }, [], ); const register = useCallback(async () => { if (password.length < MIN_PASSWORD_LENGTH) { setError(`Choose a password of at least ${MIN_PASSWORD_LENGTH} characters.`); return; } if (password !== confirm) { setError('The two passwords do not match.'); return; } setIsSubmitting(true); setError(null); const res = await authRepository.register({ code: code.trim(), fullName: fullName.trim(), password, }); if (!res.ok || !res.data) { setIsSubmitting(false); setError(res.message ?? 'Could not create your account. Please try again.'); return; } // The cookies are set; let the provider read them before navigating, so // the workspace does not paint its signed-out state first. isSubmitting // stays true until the navigation commits, as on the sign-in form. await refresh(); router.replace(destinationForUser(res.data.user)); router.refresh(); }, [code, fullName, password, confirm, refresh, router]); const submit = useCallback( (event: React.FormEvent) => { event.preventDefault(); if (isSubmitting) return; void (preview ? register() : check(code)); }, [isSubmitting, preview, register, check, code], ); /** Back to step one, for somebody holding the wrong code. */ const reset = useCallback(() => { setPreview(null); setPassword(''); setConfirm(''); setError(null); }, []); return { code, preview, fullName, password, confirm, error, isSubmitting, setCode, setFullName, setPassword, setConfirm, check, submit, reset, }; }