Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions src/lib/components/account/sendVerificationEmailModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Modal } from '$lib/components';
import { Button } from '$lib/elements/forms';
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';

let { show = $bindable(false) } = $props();
let creating = $state(false);
let emailSent = $state(false);

let cleanUrl = $derived(page.url.origin + page.url.pathname);

async function onSubmit() {
if (creating) return;
creating = true;
try {
await sdk.forConsole.account.createVerification({ url: cleanUrl });
addNotification({ message: 'Verification email has been sent', type: 'success' });
emailSent = true;
show = false;
} catch (error) {
addNotification({ message: error.message, type: 'error' });
} finally {
creating = false;
}
}

async function updateEmailVerification() {
const searchParams = page.url.searchParams;
const userId = searchParams.get('userId');
const secret = searchParams.get('secret');

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)
]);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
}

onMount(() => {
updateEmailVerification();
});
</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>
50 changes: 9 additions & 41 deletions src/lib/components/alerts/emailVerificationBanner.svelte
Original file line number Diff line number Diff line change
@@ -1,63 +1,31 @@
<script lang="ts">
import { base } from '$app/paths';
import { goto } from '$app/navigation';
import { Button } from '$lib/elements/forms';
import { HeaderAlert } from '$lib/layout';
import { Typography } from '@appwrite.io/pink-svelte';
import { hideNotification, shouldShowNotification } from '$lib/helpers/notifications';
import { user } from '$lib/stores/user';
import { wizard } from '$lib/stores/wizard';
import { page } from '$app/state';

const { emailBannerClosed, onEmailBannerClose } = $props<{
emailBannerClosed: boolean;
onEmailBannerClose: (closed: boolean) => void;
}>();

const isOnOnboarding = $derived(page.url?.pathname?.includes('/console/onboarding'));
import SendVerificationEmailModal from '../account/sendVerificationEmailModal.svelte';
import { page } from '$app/stores';

const hasUser = $derived(!!$user);
const needsEmailVerification = $derived(hasUser && !$user.emailVerification);
const shouldShowNotificationBanner = $derived.by(() =>
shouldShowNotification('email-verification-banner')
);
const wizardNotActive = $derived(!$wizard.show && !$wizard.cover);
const bannerNotClosed = $derived(!emailBannerClosed);
const notOnOnboarding = $derived(!isOnOnboarding);

const shouldShowEmailBanner = $derived(
hasUser &&
needsEmailVerification &&
shouldShowNotificationBanner &&
wizardNotActive &&
bannerNotClosed &&
notOnOnboarding
);

function navigateToAccount() {
goto(`${base}/account`);
}
const notOnOnboarding = $derived(!$page.route.id.includes('/onboarding'));
const shouldShowEmailBanner = $derived(hasUser && needsEmailVerification && notOnOnboarding);

function handleDismiss() {
onEmailBannerClose(true);
hideNotification('email-verification-banner', { coolOffPeriod: 24 * 365 * 100 });
}
let showSendVerification = $state(false);
</script>

{#if shouldShowEmailBanner}
<HeaderAlert
type="warning"
title="Your email address needs to be verified"
dismissible
on:dismiss={handleDismiss}>
<HeaderAlert type="warning" title="Your email address needs to be verified">
<svelte:fragment>
To avoid losing access to your projects, make sure <Typography.Text
variant="m-500"
style="display:inline">{$user.email}</Typography.Text> is valid and up to date. Email
verification will be required soon.
</svelte:fragment>
<svelte:fragment slot="buttons">
<Button secondary size="s" on:click={navigateToAccount}>Update email address</Button>
<Button secondary size="s" on:click={() => (showSendVerification = true)}
>Verify email</Button>
</svelte:fragment>
</HeaderAlert>
<SendVerificationEmailModal bind:show={showSendVerification} />
{/if}
5 changes: 1 addition & 4 deletions src/routes/(console)/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@
import type { LayoutData } from './$types';

export let data: LayoutData;
let emailBannerClosed = false;

function kebabToSentenceCase(str: string) {
return str
Expand Down Expand Up @@ -347,9 +346,7 @@
<Footer slot="footer" />
</Shell>

<EmailVerificationBanner
{emailBannerClosed}
onEmailBannerClose={(closed) => (emailBannerClosed = closed)} />
<EmailVerificationBanner />

{#if $wizard.show && $wizard.component}
<svelte:component this={$wizard.component} {...$wizard.props} />
Expand Down
10 changes: 9 additions & 1 deletion src/routes/(console)/account/updateEmail.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { sdk } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
import { onMount } from 'svelte';
import { Badge, Layout } from '@appwrite.io/pink-svelte';

let email: string = null;
let emailPassword: string = null;
Expand Down Expand Up @@ -40,7 +41,14 @@

<Form onSubmit={updateEmail}>
<CardGrid>
<svelte:fragment slot="title">Email</svelte:fragment>
<svelte:fragment slot="title">
<Layout.Stack direction="row" gap="s" alignItems="center">
Email
{#if $user.emailVerification}
<Badge variant="secondary" type="success" size="s" content="verified" />
{/if}
</Layout.Stack>
</svelte:fragment>
<svelte:fragment slot="aside">
<InputText
id="email"
Expand Down
24 changes: 17 additions & 7 deletions src/routes/(console)/account/updateMfa.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import { factors } from './store';
import MfaRecoveryCodes from './mfaRecoveryCodes.svelte';
import type { Models } from '@appwrite.io/console';
import { AuthenticationFactor } from '@appwrite.io/console';
import MfaRegenerateCodes from './mfaRegenerateCodes.svelte';
import { page } from '$app/state';
import { onMount } from 'svelte';
Expand Down Expand Up @@ -56,15 +57,11 @@
const userId = searchParams.get('userId');
const secret = searchParams.get('secret');

history.replaceState(null, '', cleanUrl);

if (userId && secret) {
history.replaceState(null, '', cleanUrl);
try {
await sdk.forConsole.account.updateVerification({ userId, secret });
addNotification({
message: 'Email verified successfully',
type: 'success'
});
// Don't show notification here - the modal will handle it
await Promise.all([
invalidate(Dependencies.ACCOUNT),
invalidate(Dependencies.FACTORS)
Expand All @@ -81,7 +78,20 @@
async function updateMfa() {
try {
await sdk.forConsole.account.updateMFA({ mfa: !$user.mfa });
await invalidate(Dependencies.ACCOUNT);

if (!$user.mfa && $user.emailVerification && !$factors.email) {
// Automatically set up email MFA when enabling MFA if the user has a verified email
// This provides a fallback authentication method without requiring user interaction
try {
await sdk.forConsole.account.createMFAChallenge({
factor: AuthenticationFactor.Email
});
} catch (emailError) {
// Silently ignore - email MFA is optional and shouldn't block MFA enablement
}
}

await Promise.all([invalidate(Dependencies.ACCOUNT), invalidate(Dependencies.FACTORS)]);
addNotification({
message: `Multi-factor authentication has been ${$user.mfa ? 'enabled' : 'disabled'}`,
type: 'success'
Expand Down