mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 01:41:08 +02:00
✨ 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:
parent
2ccd063628
commit
09b8d7b384
12 changed files with 162 additions and 2 deletions
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
×
|
||||
</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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue