feat(auth-ui): show email verified banner on login pages

Add verified banner and email pre-fill to LoginPage component when
users are redirected after email verification. Updates all app login
pages to pass verification params from URL query string.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-01-27 01:31:55 +01:00
parent 2ccd063628
commit 09b8d7b384
12 changed files with 162 additions and 2 deletions

View file

@ -32,6 +32,10 @@
// Get translations based on current locale
const translations = $derived(getLoginTranslations($locale || 'de'));
// Read verification status from query params (set after email verification)
const verified = $derived($page.url.searchParams.get('verified') === 'true');
const initialEmail = $derived($page.url.searchParams.get('email') || '');
async function handleSignIn(email: string, password: string) {
return authStore.signIn(email, password);
}
@ -55,6 +59,8 @@
lightBackground="#e0f2fe"
darkBackground="#0c1929"
{translations}
{verified}
{initialEmail}
>
{#snippet headerControls()}
<LanguageSelector />

View file

@ -32,6 +32,10 @@
// Get translations based on current locale
const translations = $derived(getLoginTranslations($locale || 'de'));
// Read verification status from query params (set after email verification)
const verified = $derived($page.url.searchParams.get('verified') === 'true');
const initialEmail = $derived($page.url.searchParams.get('email') || '');
async function handleSignIn(email: string, password: string) {
return authStore.signIn(email, password);
}
@ -55,6 +59,8 @@
lightBackground="#e0f2fe"
darkBackground="#0c1929"
{translations}
{verified}
{initialEmail}
>
{#snippet headerControls()}
<LanguageSelector />

View file

@ -9,6 +9,10 @@
let error = $state('');
let loading = $state(false);
// Read verification status from query params (set after email verification)
const verified = $derived($page.url.searchParams.get('verified') === 'true');
const initialEmail = $derived($page.url.searchParams.get('email') || '');
// Get redirect URL from query params or sessionStorage (set by AuthGateModal in guest mode)
const redirectTo = $derived.by(() => {
const queryRedirect = $page.url.searchParams.get('redirectTo');
@ -51,4 +55,6 @@
onSubmit={handleLogin}
registerHref="/register"
forgotPasswordHref="/forgot-password"
{verified}
{initialEmail}
/>

View file

@ -29,6 +29,10 @@
// Get translations based on current locale
const translations = $derived(getLoginTranslations($locale || 'de'));
// Read verification status from query params (set after email verification)
const verified = $derived($page.url.searchParams.get('verified') === 'true');
const initialEmail = $derived($page.url.searchParams.get('email') || '');
async function handleSignIn(email: string, password: string) {
return authStore.signIn(email, password);
}
@ -52,6 +56,8 @@
lightBackground="#eff6ff"
darkBackground="#1e293b"
{translations}
{verified}
{initialEmail}
>
{#snippet headerControls()}
<LanguageSelector />

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { locale } from 'svelte-i18n';
import { LoginPage } from '@manacore/shared-auth-ui';
import { ManaCoreLogo } from '@manacore/shared-branding';
@ -11,6 +12,10 @@
// Get translations based on current locale
const translations = $derived(getLoginTranslations($locale || 'de'));
// Read verification status from query params (set after email verification)
const verified = $derived($page.url.searchParams.get('verified') === 'true');
const initialEmail = $derived($page.url.searchParams.get('email') || '');
async function handleSignIn(email: string, password: string) {
return authStore.signIn(email, password);
}
@ -30,6 +35,8 @@
lightBackground="#f3f4f6"
darkBackground="#121212"
{translations}
{verified}
{initialEmail}
>
{#snippet headerControls()}
<LanguageSelector />

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { locale } from 'svelte-i18n';
import { LoginPage } from '@manacore/shared-auth-ui';
import { ManaDeckLogo } from '@manacore/shared-branding';
@ -11,6 +12,10 @@
// Get translations based on current locale
const translations = $derived(getLoginTranslations($locale || 'de'));
// Read verification status from query params (set after email verification)
const verified = $derived($page.url.searchParams.get('verified') === 'true');
const initialEmail = $derived($page.url.searchParams.get('email') || '');
async function handleSignIn(email: string, password: string) {
return authStore.signIn(email, password);
}
@ -30,6 +35,8 @@
lightBackground="#faf5ff"
darkBackground="#1a1625"
{translations}
{verified}
{initialEmail}
>
{#snippet headerControls()}
<LanguageSelector />

View file

@ -26,6 +26,10 @@
// German translations (NutriPhi is German-focused)
const translations = $derived(getLoginTranslations('de'));
// Read verification status from query params (set after email verification)
const verified = $derived($page.url.searchParams.get('verified') === 'true');
const initialEmail = $derived($page.url.searchParams.get('email') || '');
async function handleSignIn(email: string, password: string) {
return authStore.signIn(email, password);
}
@ -49,4 +53,6 @@
lightBackground="#dcfce7"
darkBackground="#052e16"
{translations}
{verified}
{initialEmail}
/>

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { locale } from 'svelte-i18n';
import { LoginPage, setGoogleClientId } from '@manacore/shared-auth-ui';
import { getLoginTranslations } from '@manacore/shared-i18n';
@ -32,6 +33,10 @@
// TODO: Implement OAuth with Mana Core Auth when ready
return { success: false, error: 'Apple Sign-In not yet implemented' };
}
// Read verification status from query params (set after email verification)
const verified = $derived($page.url.searchParams.get('verified') === 'true');
const initialEmail = $derived($page.url.searchParams.get('email') || '');
</script>
<svelte:head>
@ -54,6 +59,8 @@
lightBackground="#f0f9ff"
darkBackground="#0c1929"
{translations}
{verified}
{initialEmail}
>
{#snippet headerControls()}
<LanguageSelector />

View file

@ -1,11 +1,27 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { authStore } from '$lib/stores/auth.svelte';
// Read verification status from query params (set after email verification)
const verified = $derived($page.url.searchParams.get('verified') === 'true');
const initialEmailFromUrl = $derived($page.url.searchParams.get('email') || '');
let email = $state('');
let password = $state('');
let error = $state('');
let loading = $state(false);
let showVerifiedBanner = $state(false);
// Initialize email from URL if provided
$effect(() => {
if (initialEmailFromUrl && !email) {
email = initialEmailFromUrl;
}
if (verified) {
showVerifiedBanner = true;
}
});
async function handleSubmit(e: Event) {
e.preventDefault();
@ -22,9 +38,28 @@
loading = false;
}
function dismissBanner() {
showVerifiedBanner = false;
}
</script>
<form onsubmit={handleSubmit} class="space-y-4">
{#if showVerifiedBanner}
<div
class="relative rounded-md bg-green-100 p-3 text-sm text-green-800 dark:bg-green-900/30 dark:text-green-200"
>
<button
type="button"
onclick={dismissBanner}
class="absolute right-2 top-2 text-green-600 hover:text-green-800 dark:text-green-400 dark:hover:text-green-200"
>
×
</button>
E-Mail erfolgreich bestätigt! Du kannst dich jetzt anmelden.
</div>
{/if}
{#if error}
<div class="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
{error}

View file

@ -16,6 +16,10 @@
// Get translations based on current locale
const translations = $derived(getLoginTranslations($locale || 'de'));
// Read verification status from query params (set after email verification)
const verified = $derived($page.url.searchParams.get('verified') === 'true');
const initialEmail = $derived($page.url.searchParams.get('email') || '');
async function handleSignIn(email: string, password: string) {
return auth.login(email, password);
}
@ -39,6 +43,8 @@
lightBackground="#fff7ed"
darkBackground="#1c1210"
{translations}
{verified}
{initialEmail}
>
{#snippet headerControls()}
<LanguageSelector />

View file

@ -31,6 +31,10 @@
// Get translations based on current locale
const translations = $derived(getLoginTranslations($locale || 'de'));
// Read verification status from query params (set after email verification)
const verified = $derived($page.url.searchParams.get('verified') === 'true');
const initialEmail = $derived($page.url.searchParams.get('email') || '');
async function handleSignIn(email: string, password: string) {
return authStore.signIn(email, password);
}
@ -54,6 +58,8 @@
lightBackground="#f3e8ff"
darkBackground="#1e1b4b"
{translations}
{verified}
{initialEmail}
>
{#snippet headerControls()}
<LanguageSelector />

View file

@ -29,6 +29,7 @@
googleSignInFailed: string;
signInSuccess: string;
googleSignInSuccess: string;
emailVerified?: string;
}
const defaultTranslations: LoginTranslations = {
@ -54,6 +55,7 @@
googleSignInFailed: 'Google sign in failed',
signInSuccess: 'Successfully signed in. Redirecting...',
googleSignInSuccess: 'Successfully signed in with Google. Redirecting...',
emailVerified: 'Email successfully verified! Please sign in.',
};
interface Props {
@ -74,6 +76,10 @@
appSlider?: Snippet;
headerControls?: Snippet;
translations?: Partial<LoginTranslations>;
/** Show email verified success banner */
verified?: boolean;
/** Pre-fill email field (e.g., after email verification) */
initialEmail?: string;
}
let {
@ -94,6 +100,8 @@
appSlider,
headerControls,
translations = {},
verified = false,
initialEmail = '',
}: Props = $props();
const t = $derived({ ...defaultTranslations, ...translations });
@ -101,7 +109,7 @@
let loading = $state(false);
let error = $state<string | null>(null);
let errorField = $state<'email' | 'password' | 'general' | null>(null);
let email = $state('');
let email = $state(initialEmail);
let password = $state('');
let showPassword = $state(false);
let rememberMe = $state(false);
@ -110,6 +118,7 @@
let emailInput: HTMLInputElement;
let passwordInput: HTMLInputElement;
let successAnnouncement = $state('');
let showVerifiedBanner = $state(verified);
// Theme state - can be toggled manually, defaults to system preference
let userThemePreference = $state<'light' | 'dark' | null>(null);
@ -145,7 +154,12 @@
}
$effect(() => {
if (emailInput) emailInput.focus();
// Focus password field if email is pre-filled, otherwise focus email
if (initialEmail && passwordInput) {
passwordInput.focus();
} else if (emailInput) {
emailInput.focus();
}
});
function isValidEmail(email: string): boolean {
@ -296,6 +310,21 @@
<!-- Form Section -->
<div class="form-section">
<div class="form-card" class:shake={shakeError}>
{#if showVerifiedBanner}
<div class="verified-banner" role="status" aria-live="polite">
<Check size={18} class="text-green-500 shrink-0" />
<p>{t.emailVerified}</p>
<button
type="button"
class="verified-banner-close"
onclick={() => (showVerifiedBanner = false)}
aria-label="Close"
>
&times;
</button>
</div>
{/if}
<div class="form-header">
<h2 class="form-title">{t.title}</h2>
<p class="form-subtitle">{t.subtitle}</p>
@ -612,6 +641,39 @@
color: rgba(0, 0, 0, 0.6);
}
.verified-banner {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem;
margin-bottom: 1rem;
border-radius: 0.75rem;
background: rgba(34, 197, 94, 0.15);
border: 1px solid rgba(34, 197, 94, 0.3);
color: #22c55e;
font-size: 0.875rem;
position: relative;
}
.verified-banner-close {
position: absolute;
right: 0.5rem;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #22c55e;
font-size: 1.25rem;
cursor: pointer;
padding: 0.25rem;
line-height: 1;
opacity: 0.7;
}
.verified-banner-close:hover {
opacity: 1;
}
.error-message {
display: flex;
align-items: center;