-
Notifications
You must be signed in to change notification settings - Fork 191
feat: implement blocking email verification modal #2379
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ae5dc7f
9019030
ed72736
efc877b
3e107fd
ced31fa
8d039c0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -2,36 +2,117 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||
import { invalidate } from '$app/navigation'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { Modal } from '$lib/components'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { Button } from '$lib/elements/forms'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import Link from '$lib/elements/link.svelte'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { addNotification } from '$lib/stores/notifications'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { sdk } from '$lib/stores/sdk'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { user } from '$lib/stores/user'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { get } from 'svelte/store'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { page } from '$app/state'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { Card, Layout, Typography } from '@appwrite.io/pink-svelte'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { Dependencies } from '$lib/constants'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { onMount } from 'svelte'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { onMount, onDestroy } from 'svelte'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { isCloud } from '$lib/system'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { wizard, isNewWizardStatusOpen } from '$lib/stores/wizard'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { logout } from '$lib/helpers/logout'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { browser } from '$app/environment'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
let { show = $bindable(false) } = $props(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
let { show = $bindable(false), email: emailFromParent = null } = $props(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
let creating = $state(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
let emailSent = $state(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
let resendTimer = $state(0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
let timerInterval: ReturnType<typeof setInterval> | null = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Timer state key for localStorage | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const TIMER_END_KEY = 'email-verification-timer-end'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const EMAIL_SENT_KEY = 'email-verification-sent'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
let cleanUrl = $derived(page.url.origin + page.url.pathname); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const resolvedEmail = $derived(emailFromParent ?? get(user)?.email); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
async function onSubmit() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (creating) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Determine if we should show the modal | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const hasUser = $derived(!!$user); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const needsEmailVerification = $derived(hasUser && !$user.emailVerification); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const notOnOnboarding = $derived(!page.route.id.includes('/onboarding')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const notOnWizard = $derived(!$wizard.show && !$isNewWizardStatusOpen); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const shouldShowModal = $derived( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
isCloud && hasUser && needsEmailVerification && notOnOnboarding && notOnWizard | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
function startResendTimer() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const timerEndTime = Date.now() + 60 * 1000; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
resendTimer = 60; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
emailSent = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (browser) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
localStorage.setItem(TIMER_END_KEY, timerEndTime.toString()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
localStorage.setItem(EMAIL_SENT_KEY, 'true'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
startTimerCountdown(timerEndTime); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
function restoreTimerState() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (!browser) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
const savedTimerEnd = localStorage.getItem(TIMER_END_KEY); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const savedEmailSent = localStorage.getItem(EMAIL_SENT_KEY); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (savedTimerEnd && savedEmailSent) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const timerEndTime = parseInt(savedTimerEnd); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const now = Date.now(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const remainingTime = Math.max(0, Math.ceil((timerEndTime - now) / 1000)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (remainingTime > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
resendTimer = remainingTime; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
emailSent = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
startTimerCountdown(timerEndTime); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Timer has expired, clean up | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
localStorage.removeItem(TIMER_END_KEY); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
localStorage.removeItem(EMAIL_SENT_KEY); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
resendTimer = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
emailSent = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
function startTimerCountdown(timerEndTime: number) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
timerInterval = setInterval(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const now = Date.now(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const remainingTime = Math.max(0, Math.ceil((timerEndTime - now) / 1000)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
resendTimer = remainingTime; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (remainingTime <= 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
clearInterval(timerInterval); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
timerInterval = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (browser) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
localStorage.removeItem(TIMER_END_KEY); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
localStorage.removeItem(EMAIL_SENT_KEY); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, 1000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
async function sendVerificationEmail() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (creating || resendTimer > 0) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
creating = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
await sdk.forConsole.account.createVerification({ url: cleanUrl }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
addNotification({ message: 'Verification email has been sent', type: 'success' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
emailSent = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
show = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
startResendTimer(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Don't close modal - user needs to verify email first | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
addNotification({ message: error.message, type: 'error' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
creating = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
async function onSubmit() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
await sendVerificationEmail(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
async function updateEmailVerification() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const searchParams = page.url.searchParams; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const userId = searchParams.get('userId'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -40,10 +121,6 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||
if (userId && secret) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
await sdk.forConsole.account.updateVerification({ userId, secret }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
addNotification({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
message: 'Email verified successfully', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
type: 'success' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
await Promise.all([ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
invalidate(Dependencies.ACCOUNT), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
invalidate(Dependencies.FACTORS) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -59,21 +136,72 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
onMount(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
updateEmailVerification(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
restoreTimerState(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
onDestroy(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (timerInterval) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
clearInterval(timerInterval); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
// round up localstorage when component is destroyed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (browser) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
localStorage.removeItem(TIMER_END_KEY); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
localStorage.removeItem(EMAIL_SENT_KEY); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+142
to
151
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't clear localStorage on destroy; it defeats persistence of the resend timer. Clearing Apply this diff: onDestroy(() => {
if (timerInterval) {
clearInterval(timerInterval);
}
- // round up localstorage when component is destroyed
- if (browser) {
- localStorage.removeItem(TIMER_END_KEY);
- localStorage.removeItem(EMAIL_SENT_KEY);
- }
}); 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
</script> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Modal bind:show title="Send verification email" {onSubmit}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Card.Base variant="secondary" padding="s"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Layout.Stack gap="m"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Typography.Text gap="m"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
To continue using Appwrite Cloud, please verify your email address. An email will be | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
sent to <Typography.Text variant="m-600" style="display: inline;" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
>{get(user)?.email}</Typography.Text> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</Typography.Text> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</Layout.Stack> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</Card.Base> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
<svelte:fragment slot="footer"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Button submit disabled={creating}>{emailSent ? 'Resend email' : 'Send email'}</Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</svelte:fragment> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</Modal> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
{#if shouldShowModal || show} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<div class="email-verification-scrim"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Modal | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
show={true} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
title="Verify your email address" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
{onSubmit} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
dismissible={false} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
autoClose={false}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Card.Base variant="secondary" padding="s"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Layout.Stack gap="xxs"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Typography.Text gap="m"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
To continue using Appwrite Cloud, please verify your email address. An email | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
will be sent to <Typography.Text | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
variant="m-600" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
color="neutral-secondary" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
style="display: inline;">{resolvedEmail}</Typography.Text> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</Typography.Text> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Layout.Stack class="u-margin-block-start-4 u-margin-block-end-24"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Layout.Stack direction="row"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Link variant="default" on:click={() => logout(false)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
>Switch account</Link> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</Layout.Stack> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</Layout.Stack> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
{#if emailSent && resendTimer > 0} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Typography.Text color="neutral-secondary"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Didn't get the email? Try again in {resendTimer}s | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</Typography.Text> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
{/if} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</Layout.Stack> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</Card.Base> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
<svelte:fragment slot="footer"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Button submit disabled={creating || resendTimer > 0}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
{emailSent ? 'Resend email' : 'Send email'} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</svelte:fragment> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</Modal> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
{/if} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
<style> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
.email-verification-scrim { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
position: fixed; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
top: 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
left: 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
width: 100%; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
height: 100%; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
background-color: hsl(240 5% 8% / 0.6); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
backdrop-filter: blur(4px); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
display: flex; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
align-items: center; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
justify-content: center; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</style> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+194
to
+207
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure the scrim overlays all console content. Add a z-index and simplify positioning; without z-index the header/other fixed elements may sit above the scrim. .email-verification-scrim {
position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
+ inset: 0;
background-color: hsl(240 5% 8% / 0.6);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
+ z-index: var(--z-modal, 1000);
} 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
<script lang="ts"> | ||
// verify email layout | ||
</script> | ||
|
||
<slot /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Namespace timer keys per account to avoid cross‑account bleed.
Using global keys means logging out and switching users within 60s incorrectly throttles the next user. Namespace by
user.$id
(fallback to email) and update all usages.Also applies to: 46-48, 56-58, 70-71, 89-91