feat(auth): add resend verification email to registration screen

- Add prominent email verification success UI with resend button
- Show resend verification option when registration fails with "not verified" error
- Improve form spacing with space-y-4 for better visual consistency
- Add translations for resend verification in all languages (de, en, fr, it, es)
- Update all 13 app register pages to pass onResendVerification prop

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-02-01 12:44:28 +01:00
parent 1c6505890d
commit d703ccfd80
20 changed files with 236 additions and 18 deletions

View file

@ -29,6 +29,10 @@
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
}
async function handleResendVerification(email: string) {
return authStore.resendVerificationEmail(email);
}
</script>
<svelte:head>
@ -40,6 +44,7 @@
logo={CalendarLogo}
primaryColor="#0ea5e9"
onSignUp={handleSignUp}
onResendVerification={handleResendVerification}
{goto}
successRedirect={redirectTo}
loginPath="/login"

View file

@ -29,6 +29,10 @@
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
}
async function handleResendVerification(email: string) {
return authStore.resendVerificationEmail(email);
}
</script>
<svelte:head>
@ -40,6 +44,7 @@
logo={ChatLogo}
primaryColor="#0ea5e9"
onSignUp={handleSignUp}
onResendVerification={handleResendVerification}
{goto}
successRedirect={redirectTo}
loginPath="/login"

View file

@ -26,6 +26,10 @@
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
}
async function handleResendVerification(email: string) {
return authStore.resendVerificationEmail(email);
}
</script>
<svelte:head>
@ -37,6 +41,7 @@
logo={ContactsLogo}
primaryColor="#3b82f6"
onSignUp={handleSignUp}
onResendVerification={handleResendVerification}
{goto}
successRedirect={redirectTo}
loginPath="/login"

View file

@ -16,6 +16,10 @@
async function handleValidateReferralCode(code: string) {
return authStore.validateReferralCode(code);
}
async function handleResendVerification(email: string) {
return authStore.resendVerificationEmail(email);
}
</script>
<RegisterPage
@ -23,6 +27,7 @@
logo={ManaCoreLogo}
primaryColor="#6366f1"
onSignUp={handleSignUp}
onResendVerification={handleResendVerification}
onValidateReferralCode={handleValidateReferralCode}
{initialReferralCode}
baseSignupCredits={25}

View file

@ -8,6 +8,10 @@
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
}
async function handleResendVerification(email: string) {
return authStore.resendVerificationEmail(email);
}
</script>
<RegisterPage
@ -15,6 +19,7 @@
logo={ManaDeckLogo}
primaryColor="#8b5cf6"
onSignUp={handleSignUp}
onResendVerification={handleResendVerification}
{goto}
successRedirect="/decks"
loginPath="/login"

View file

@ -23,6 +23,10 @@
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
}
async function handleResendVerification(email: string) {
return authStore.resendVerificationEmail(email);
}
</script>
<svelte:head>
@ -34,6 +38,7 @@
logo={NutriPhiLogo}
primaryColor="#22C55E"
onSignUp={handleSignUp}
onResendVerification={handleResendVerification}
{goto}
successRedirect={redirectTo}
loginPath="/login"

View file

@ -11,6 +11,10 @@
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
}
async function handleResendVerification(email: string) {
return authStore.resendVerificationEmail(email);
}
</script>
<svelte:head>
@ -22,6 +26,7 @@
logo={PictureLogo}
primaryColor="#3b82f6"
onSignUp={handleSignUp}
onResendVerification={handleResendVerification}
{goto}
successRedirect="/app/gallery"
loginPath="/auth/login"

View file

@ -15,6 +15,10 @@
async function handleSignUp(email: string, password: string) {
return auth.register(email, password);
}
async function handleResendVerification(email: string) {
return auth.resendVerificationEmail(email);
}
</script>
<svelte:head>
@ -26,6 +30,7 @@
logo={PresiLogo}
primaryColor="#f97316"
onSignUp={handleSignUp}
onResendVerification={handleResendVerification}
{goto}
successRedirect="/"
loginPath="/login"

View file

@ -24,6 +24,10 @@
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
}
async function handleResendVerification(email: string) {
return authStore.resendVerificationEmail(email);
}
</script>
<svelte:head>
@ -35,6 +39,7 @@
logo={QuestionsLogo}
primaryColor="#8b5cf6"
onSignUp={handleSignUp}
onResendVerification={handleResendVerification}
{goto}
successRedirect={redirectTo}
loginPath="/login"

View file

@ -24,6 +24,10 @@
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
}
async function handleResendVerification(email: string) {
return authStore.resendVerificationEmail(email);
}
</script>
<svelte:head>
@ -35,6 +39,7 @@
logo={SkillTreeLogo}
primaryColor="#10b981"
onSignUp={handleSignUp}
onResendVerification={handleResendVerification}
{goto}
successRedirect={redirectTo}
loginPath="/login"

View file

@ -13,6 +13,10 @@
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
}
async function handleResendVerification(email: string) {
return authStore.resendVerificationEmail(email);
}
</script>
<svelte:head>
@ -24,6 +28,7 @@
logo={ManaIcon}
primaryColor="#3b82f6"
onSignUp={handleSignUp}
onResendVerification={handleResendVerification}
{goto}
successRedirect="/files"
loginPath="/login"

View file

@ -28,6 +28,10 @@
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
}
async function handleResendVerification(email: string) {
return authStore.resendVerificationEmail(email);
}
</script>
<svelte:head>
@ -39,6 +43,7 @@
logo={TodoLogo}
primaryColor="#8b5cf6"
onSignUp={handleSignUp}
onResendVerification={handleResendVerification}
{goto}
successRedirect={redirectTo}
loginPath="/login"

View file

@ -13,6 +13,10 @@
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
}
async function handleResendVerification(email: string) {
return authStore.resendVerificationEmail(email);
}
</script>
<svelte:head>
@ -24,6 +28,7 @@
logo={ZitareLogo}
primaryColor="#f59e0b"
onSignUp={handleSignUp}
onResendVerification={handleResendVerification}
{goto}
successRedirect="/"
loginPath="/login"

View file

@ -33,6 +33,11 @@
registrationFailed: string;
// Success messages
accountCreated: string;
// Verification resend
resendVerification?: string;
resendingVerification?: string;
verificationEmailSent?: string;
checkYourEmail?: string;
}
/** Referral code validation result */
@ -72,6 +77,11 @@
'Password must include lowercase, uppercase, number, and special character',
registrationFailed: 'Registration failed',
accountCreated: 'Account created! Please check your email to verify your account.',
// Verification resend
resendVerification: 'Resend verification email',
resendingVerification: 'Sending...',
verificationEmailSent: 'Verification email sent! Please check your inbox.',
checkYourEmail: 'Check your email',
};
interface Props {
@ -85,6 +95,8 @@
onSignUp: (email: string, password: string, referralCode?: string) => Promise<AuthResult>;
/** Navigation function */
goto: (path: string) => void;
/** Resend verification email function */
onResendVerification?: (email: string) => Promise<AuthResult>;
/** Success redirect path */
successRedirect?: string;
/** Login page path */
@ -113,6 +125,7 @@
primaryColor,
onSignUp,
goto,
onResendVerification,
successRedirect = '/dashboard',
loginPath = '/login',
lightBackground = '#f5f5f5',
@ -137,6 +150,8 @@
let confirmPassword = $state('');
let showPassword = $state(false);
let showConfirmPassword = $state(false);
let resendingVerification = $state(false);
let verificationEmailSent = $state(false);
// Referral state
let referralCode = $state(initialReferralCode);
@ -310,6 +325,22 @@
error = result.error || t.registrationFailed;
}
}
async function handleResendVerification() {
if (!onResendVerification || !email || resendingVerification) return;
resendingVerification = true;
error = null;
const result = await onResendVerification(email);
resendingVerification = false;
if (result.success) {
verificationEmailSent = true;
} else {
error = result.error || t.registrationFailed;
}
}
</script>
<svelte:head>
@ -390,24 +421,122 @@
</div>
{/if}
<!-- Success Message -->
{#if success && needsVerification}
<div class="mb-4 rounded-xl bg-green-500/20 border border-green-500/30 p-3">
<p class="text-sm text-green-500">
{t.accountCreated}
<!-- Verification Email Sent Confirmation -->
{#if verificationEmailSent}
<div class="mb-4 rounded-xl bg-green-500/20 border border-green-500/30 p-4">
<p class="text-sm text-green-500 font-medium">
{t.verificationEmailSent}
</p>
</div>
{/if}
<!-- Success Message with Resend Option -->
{#if success && needsVerification}
<div
class="mb-6 rounded-xl p-5"
style="background-color: {isDark
? 'rgba(34, 197, 94, 0.15)'
: 'rgba(34, 197, 94, 0.1)'}; border: 2px solid rgba(34, 197, 94, 0.4);"
>
<div class="flex items-start gap-3 mb-4">
<div
class="flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center"
style="background-color: rgba(34, 197, 94, 0.2);"
>
<svg
class="w-5 h-5 text-green-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
></path>
</svg>
</div>
<div>
<h3
class="font-semibold text-base mb-1"
style="color: {isDark ? '#22c55e' : '#16a34a'};"
>
{t.checkYourEmail || 'Check your email'}
</h3>
<p
class="text-sm"
style="color: {isDark ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.7)'};"
>
{t.accountCreated}
</p>
</div>
</div>
{#if onResendVerification}
<div
class="pt-3 border-t"
style="border-color: {isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'};"
>
<p
class="text-xs mb-2"
style="color: {isDark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)'};"
>
Didn't receive the email?
</p>
<button
type="button"
onclick={handleResendVerification}
disabled={resendingVerification}
class="w-full flex items-center justify-center gap-2 h-11 rounded-lg font-medium transition-all hover:opacity-80 disabled:opacity-50"
style="background-color: {primaryColor}40; border: 1.5px solid {primaryColor}; color: {isDark
? '#ffffff'
: '#000000'};"
>
{#if resendingVerification}
<svg class="animate-spin h-4 w-4" viewBox="0 0 24 24">
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
fill="none"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
{t.resendingVerification}
{:else}
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
></path>
</svg>
{t.resendVerification}
{/if}
</button>
</div>
{/if}
</div>
{/if}
<!-- Register Form -->
<form
onsubmit={(e) => {
e.preventDefault();
handleRegister();
}}
class="pb-4"
class="space-y-4"
>
<div class="mb-2">
<div>
<input
type="email"
bind:value={email}
@ -424,7 +553,7 @@
/>
</div>
<div class="mb-2">
<div>
<div class="relative">
<input
type={showPassword ? 'text' : 'password'}
@ -456,7 +585,7 @@
</div>
</div>
<div class="mb-2">
<div>
<div class="relative">
<input
type={showConfirmPassword ? 'text' : 'password'}
@ -490,15 +619,15 @@
<!-- Password Requirements -->
<p
class="mb-4 mt-2 text-xs"
style="color: {isDark ? 'rgba(255, 255, 255, 0.6)' : 'rgba(0, 0, 0, 0.6)'};"
class="text-xs -mt-2"
style="color: {isDark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)'};"
>
{t.passwordRequirements}
</p>
<!-- Referral Code Input -->
{#if onValidateReferralCode}
<div class="mb-4">
<div>
<div class="relative">
<input
type="text"
@ -551,7 +680,7 @@
<button
type="submit"
disabled={loading}
class="flex h-14 w-full items-center justify-center gap-2 rounded-xl font-medium transition-all hover:opacity-80 disabled:opacity-50 border-2"
class="flex h-14 w-full items-center justify-center gap-2 rounded-xl font-medium transition-all hover:opacity-80 disabled:opacity-50 border-2 mt-2"
style="background-color: {primaryColor}60; border-color: {primaryColor}; color: {isDark
? '#ffffff'
: '#000000'};"

View file

@ -46,7 +46,11 @@
"passwordTooShort": "Passwort muss mindestens 8 Zeichen lang sein",
"passwordStrengthError": "Passwort muss Kleinbuchstaben, Großbuchstaben, Zahl und Sonderzeichen enthalten",
"registrationFailed": "Registrierung fehlgeschlagen",
"accountCreated": "Konto erstellt! Bitte überprüfe deine E-Mails, um dein Konto zu bestätigen."
"accountCreated": "Konto erstellt! Bitte überprüfe deine E-Mails, um dein Konto zu bestätigen.",
"resendVerification": "Bestätigungs-E-Mail erneut senden",
"resendingVerification": "Wird gesendet...",
"verificationEmailSent": "Bestätigungs-E-Mail wurde gesendet! Bitte überprüfe deinen Posteingang.",
"checkYourEmail": "Überprüfe deine E-Mails"
},
"forgotPassword": {
"titleForm": "Passwort zurücksetzen",

View file

@ -46,7 +46,11 @@
"passwordTooShort": "Password must be at least 8 characters",
"passwordStrengthError": "Password must include lowercase, uppercase, number, and special character",
"registrationFailed": "Registration failed",
"accountCreated": "Account created! Please check your email to verify your account."
"accountCreated": "Account created! Please check your email to verify your account.",
"resendVerification": "Resend verification email",
"resendingVerification": "Sending...",
"verificationEmailSent": "Verification email sent! Please check your inbox.",
"checkYourEmail": "Check your email"
},
"forgotPassword": {
"titleForm": "Reset Password",

View file

@ -46,7 +46,11 @@
"passwordTooShort": "La contraseña debe tener al menos 8 caracteres",
"passwordStrengthError": "La contraseña debe incluir minúsculas, mayúsculas, números y caracteres especiales",
"registrationFailed": "Error en el registro",
"accountCreated": "¡Cuenta creada! Por favor revisa tu correo para verificar tu cuenta."
"accountCreated": "¡Cuenta creada! Por favor revisa tu correo para verificar tu cuenta.",
"resendVerification": "Reenviar correo de verificación",
"resendingVerification": "Enviando...",
"verificationEmailSent": "¡Correo de verificación enviado! Por favor revisa tu bandeja de entrada.",
"checkYourEmail": "Revisa tu correo"
},
"forgotPassword": {
"titleForm": "Restablecer Contraseña",

View file

@ -46,7 +46,11 @@
"passwordTooShort": "Le mot de passe doit contenir au moins 8 caractères",
"passwordStrengthError": "Le mot de passe doit contenir des minuscules, majuscules, chiffres et caractères spéciaux",
"registrationFailed": "Échec de l'inscription",
"accountCreated": "Compte créé ! Veuillez vérifier votre email pour activer votre compte."
"accountCreated": "Compte créé ! Veuillez vérifier votre email pour activer votre compte.",
"resendVerification": "Renvoyer l'email de vérification",
"resendingVerification": "Envoi en cours...",
"verificationEmailSent": "Email de vérification envoyé ! Veuillez vérifier votre boîte de réception.",
"checkYourEmail": "Vérifiez votre boîte mail"
},
"forgotPassword": {
"titleForm": "Réinitialiser le mot de passe",

View file

@ -62,6 +62,10 @@ export interface AuthTranslations {
passwordStrengthError: string;
registrationFailed: string;
accountCreated: string;
resendVerification?: string;
resendingVerification?: string;
verificationEmailSent?: string;
checkYourEmail?: string;
};
forgotPassword: {
titleForm: string;

View file

@ -46,7 +46,11 @@
"passwordTooShort": "La password deve contenere almeno 8 caratteri",
"passwordStrengthError": "La password deve contenere minuscole, maiuscole, numeri e caratteri speciali",
"registrationFailed": "Registrazione fallita",
"accountCreated": "Account creato! Controlla la tua email per verificare il tuo account."
"accountCreated": "Account creato! Controlla la tua email per verificare il tuo account.",
"resendVerification": "Invia di nuovo l'email di verifica",
"resendingVerification": "Invio in corso...",
"verificationEmailSent": "Email di verifica inviata! Controlla la tua casella di posta.",
"checkYourEmail": "Controlla la tua email"
},
"forgotPassword": {
"titleForm": "Reimposta Password",