Files
loyaly_cutomerweb/src/features/auth/hooks/useJoinForm.ts
2026-09-25 16:31:10 +05:30

124 lines
4.0 KiB
TypeScript

'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<InvitationPreview | null>(null);
const [fullName, setFullName] = useState('');
const [password, setPassword] = useState('');
const [confirm, setConfirm] = useState('');
const [error, setError] = useState<string | null>(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,
};
}