nicer package layout, also in checkout step 1, fixed missing registration language strings, registration error handling, email verification redirect, email verification error handling and messaging,

This commit is contained in:
Codex Agent
2025-11-19 20:21:54 +01:00
parent 91d3e61b0e
commit 8d2075bdd2
24 changed files with 1000 additions and 363 deletions

View File

@@ -37,11 +37,37 @@ type RegisterFormFields = {
package_id: number | null;
};
const getCookieValue = (name: string): string | null => {
if (typeof document === 'undefined') {
return null;
}
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
return match ? decodeURIComponent(match[1]) : null;
};
const resolveCsrfToken = (): string => {
if (typeof document === 'undefined') {
return '';
}
const metaToken = (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement | null)?.content;
if (metaToken && metaToken.length > 0) {
return metaToken;
}
return getCookieValue('XSRF-TOKEN') ?? '';
};
export default function RegisterForm({ packageId, onSuccess, privacyHtml, locale, prefill, onClearGoogleProfile }: RegisterFormProps) {
const [privacyOpen, setPrivacyOpen] = useState(false);
const [hasTriedSubmit, setHasTriedSubmit] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [prefillApplied, setPrefillApplied] = useState(false);
const [serverError, setServerError] = useState<string | null>(null);
const [serverErrorType, setServerErrorType] = useState<'generic' | 'session-expired'>('generic');
const { t } = useTranslation(['auth', 'common']);
const page = usePage<{ errors: Record<string, string>; locale?: string; auth?: { user?: any | null } }>();
const resolvedLocale = locale ?? page.props.locale ?? 'de';
@@ -68,6 +94,30 @@ export default function RegisterForm({ packageId, onSuccess, privacyHtml, locale
const registerEndpoint = '/checkout/register';
const requiredStringFields: Array<keyof RegisterFormFields> = useMemo(() => (
['first_name', 'last_name', 'username', 'email', 'password', 'password_confirmation', 'address', 'phone']
), []);
const isFormValid = useMemo(() => {
const stringsValid = requiredStringFields.every((field) => {
const value = data[field];
if (typeof value !== 'string') {
return false;
}
return value.trim().length > 0;
});
if (!stringsValid) {
return false;
}
const emailValid = /.+@.+\..+/.test(data.email.trim());
const passwordsMatch = data.password === data.password_confirmation;
return emailValid && passwordsMatch && data.privacy_consent && data.terms;
}, [data, requiredStringFields]);
const namePrefill = useMemo(() => {
const rawFirst = prefill?.given_name ?? prefill?.name?.split(' ')[0] ?? '';
const remaining = prefill?.name ? prefill.name.split(' ').slice(1).join(' ') : '';
@@ -126,24 +176,28 @@ export default function RegisterForm({ packageId, onSuccess, privacyHtml, locale
const submit = async (event: React.FormEvent) => {
event.preventDefault();
setServerError(null);
setServerErrorType('generic');
setHasTriedSubmit(true);
setIsSubmitting(true);
clearErrors();
const csrfToken = resolveCsrfToken();
const body = {
...data,
locale: resolvedLocale,
package_id: data.package_id ?? packageId ?? null,
_token: csrfToken,
};
try {
const response = await fetch(registerEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-CSRF-TOKEN': (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement | null)?.content ?? '',
'X-CSRF-TOKEN': csrfToken,
'X-XSRF-TOKEN': csrfToken,
},
credentials: 'same-origin',
body: JSON.stringify(body),
@@ -179,10 +233,32 @@ export default function RegisterForm({ packageId, onSuccess, privacyHtml, locale
return;
}
toast.error(t('register.unexpected_error', 'Registrierung nicht moeglich.'));
if (response.status === 419) {
const expiredMessage = t('register.session_expired_message', 'Deine Sitzung ist abgelaufen. Bitte lade die Seite neu und versuche es erneut.');
setServerErrorType('session-expired');
setServerError(expiredMessage);
toast.error(expiredMessage);
return;
}
let message: string | null = null;
try {
const json = await response.clone().json();
message = json?.message ?? null;
} catch (error) {
message = null;
}
const fallbackMessage = message ?? t('register.server_error_message', 'Etwas ist schiefgelaufen. Bitte versuche es erneut.');
setServerErrorType('generic');
setServerError(fallbackMessage);
toast.error(fallbackMessage);
} catch (error) {
console.error('Register request failed', error);
toast.error(t('register.unexpected_error', 'Registrierung nicht moeglich.'));
const fallbackMessage = t('register.server_error_message', 'Etwas ist schiefgelaufen. Bitte versuche es erneut.');
setServerErrorType('generic');
setServerError(fallbackMessage);
toast.error(fallbackMessage);
} finally {
setIsSubmitting(false);
}
@@ -463,10 +539,21 @@ export default function RegisterForm({ packageId, onSuccess, privacyHtml, locale
</div>
)}
{serverError && (
<div className="mb-6 rounded-md border border-red-200 bg-red-50 p-4 text-sm text-red-800">
<p className="font-semibold">
{serverErrorType === 'session-expired'
? t('register.session_expired_title', 'Sicherheitsprüfung abgelaufen')
: t('register.server_error_title', 'Registrierung konnte nicht abgeschlossen werden')}
</p>
<p className="mt-1">{serverError}</p>
</div>
)}
<button
type="button"
onClick={submit}
disabled={isSubmitting}
disabled={isSubmitting || !isFormValid}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-[#FFB6C1] hover:bg-[#FF69B4] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#FFB6C1] transition duration-300 disabled:opacity-50"
>
{isSubmitting && <LoaderCircle className="h-4 w-4 animate-spin mr-2" />}