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:
@@ -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" />}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FormEvent, useEffect, useMemo, useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Head, useForm } from '@inertiajs/react';
|
||||
import { Head, useForm, usePage } from '@inertiajs/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import InputError from '@/components/input-error';
|
||||
import TextLink from '@/components/text-link';
|
||||
@@ -13,6 +13,7 @@ import AppLayout from '@/layouts/app/AppLayout';
|
||||
import { register } from '@/routes';
|
||||
import { request } from '@/routes/password';
|
||||
import { LoaderCircle } from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
interface LoginProps {
|
||||
status?: string;
|
||||
@@ -24,6 +25,8 @@ export default function Login({ status, canResetPassword }: LoginProps) {
|
||||
const [rawReturnTo, setRawReturnTo] = useState<string | null>(null);
|
||||
const [isRedirectingToGoogle, setIsRedirectingToGoogle] = useState(false);
|
||||
const { t } = useTranslation('auth');
|
||||
const page = usePage<{ flash?: { verification?: { status: string; title?: string; message?: string } } }>();
|
||||
const verificationFlash = page.props.flash?.verification;
|
||||
|
||||
const { data, setData, post, processing, errors, clearErrors } = useForm({
|
||||
login: '',
|
||||
@@ -50,7 +53,15 @@ export default function Login({ status, canResetPassword }: LoginProps) {
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
setRawReturnTo(searchParams.get('return_to'));
|
||||
}, []);
|
||||
|
||||
if (searchParams.get('verified') === '1') {
|
||||
toast.success(t('verification.toast_success', 'Email verified successfully.'));
|
||||
searchParams.delete('verified');
|
||||
const nextQuery = searchParams.toString();
|
||||
const nextUrl = `${window.location.pathname}${nextQuery ? `?${nextQuery}` : ''}`;
|
||||
window.history.replaceState({}, '', nextUrl);
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
setData('return_to', rawReturnTo ?? '');
|
||||
@@ -213,6 +224,20 @@ export default function Login({ status, canResetPassword }: LoginProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{verificationFlash && (
|
||||
<div
|
||||
className={[
|
||||
'rounded-2xl border p-3 text-center font-medium shadow-sm',
|
||||
verificationFlash.status === 'success'
|
||||
? 'border-emerald-200/70 bg-emerald-50/90 text-emerald-700'
|
||||
: 'border-rose-200/70 bg-rose-50/90 text-rose-700',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="font-semibold">{verificationFlash.title ?? ''}</div>
|
||||
<div className="text-sm">{verificationFlash.message}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasErrors && (
|
||||
<div
|
||||
key={`general-errors-${errorKeys.join('-')}`}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Components
|
||||
import { store } from '@/actions/App/Http/Controllers/Auth/EmailVerificationNotificationController';
|
||||
import { logout } from '@/routes';
|
||||
import { Form, Head } from '@inertiajs/react';
|
||||
import { Form, Head, usePage } from '@inertiajs/react';
|
||||
import { LoaderCircle } from 'lucide-react';
|
||||
|
||||
import TextLink from '@/components/text-link';
|
||||
@@ -13,11 +13,26 @@ export default function VerifyEmail({ status }: { status?: string }) {
|
||||
const description = isNewRegistration
|
||||
? 'Thanks! Please confirm your email address to access your dashboard.'
|
||||
: 'Please verify your email address by clicking on the link we just emailed to you.';
|
||||
const page = usePage<{ flash?: { verification?: { status: string; title?: string; message?: string } } }>();
|
||||
const verificationFlash = page.props.flash?.verification;
|
||||
|
||||
return (
|
||||
<AuthLayout title="Verify email" description={description}>
|
||||
<Head title="Email verification" />
|
||||
|
||||
{verificationFlash && (
|
||||
<div
|
||||
className={`mb-6 rounded-md border p-4 text-left text-sm font-medium ${
|
||||
verificationFlash.status === 'success'
|
||||
? 'border-emerald-200 bg-emerald-50 text-emerald-800'
|
||||
: 'border-rose-200 bg-rose-50 text-rose-800'
|
||||
}`}
|
||||
>
|
||||
<p className="font-semibold">{verificationFlash.title}</p>
|
||||
<p className="mt-1 text-sm font-normal">{verificationFlash.message}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isNewRegistration && (
|
||||
<div className="mb-6 rounded-md bg-blue-50 p-4 text-left text-sm text-blue-900">
|
||||
<p className="font-semibold">Almost there! Confirm your email address to start using Fotospiel.</p>
|
||||
|
||||
Reference in New Issue
Block a user